Яке найчастіше питання про сумісність, з яким ви стикалися на Java? [зачинено]


192

Це опитування на зразок поширених проблем спільної валюти на Java. Прикладом може бути класичний тупиковий стан або стан перегонів або, можливо, помилки з потоком EDT в Swing. Мене цікавить як широта можливих питань, так і питання, які є найбільш поширеними. Отже, будь ласка, залиште одну конкретну відповідь на помилку одночасності Java на коментар і проголосуйте, якщо ви бачите одну, з якою ви стикалися.


16
Чому це закрито? Це корисно як для інших програмістів, які випрошують паралельність в Java, так і мати уявлення про те, які класи дефектів паралельної валюти спостерігаються найбільше іншими розробниками Java.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

@Longpoke Повідомлення про закриття пояснює, чому це закрито. Це не питання із конкретною "правильною" відповіддю, це більше питання опитування / списку. І Stack Overflow не збирається приймати подібні запитання. Якщо ви не згодні з цією політикою, можливо, ви захочете обговорити її на мета .
Анджей Дойл

7
Я думаю, що спільнота не погоджується, оскільки ця стаття отримує 100+ переглядів / день! Я вважаю це дуже корисним, оскільки я беру участь у розробці інструменту статичного аналізу, спеціально розробленого для виправлення проблем з одночасністю contemplateltd.com/threadsafe . Наявність у банку часто зустрічаються проблем з одночасністю було чудово підходить для тестування та вдосконалення ThreadSafe.
Крейг Менсон

Контрольний перелік коду для Java Concurrency перетворює більшість підводних каменів, зазначених у відповідях на це питання, у формі, зручній для щоденного огляду коду.
Левентов

Відповіді:


125

Найпоширеніша проблема одночасності, яку я бачив, - це не усвідомлення того, що поле, записане одним потоком, не гарантовано бачити іншим потоком. Загальне застосування цього:

class MyThread extends Thread {
  private boolean stop = false;

  public void run() {
    while(!stop) {
      doSomeWork();
    }
  }

  public void setStop() {
    this.stop = true;
  }
}

До тих пір поки зупинки не летючий або setStopі runНЕ синхронізовані це не гарантує роботу. Ця помилка особливо диявольська, оскільки в 99,999% це не має значення на практиці, оскільки читацька нитка зрештою побачить зміни - але ми не знаємо, як скоро він це побачив.


9
Прекрасним рішенням для цього є зробити змінну зупинки екземпляра AtomicBoolean. Він вирішує всі проблеми енергонезалежних, захищаючи вас від проблем JMM.
Кірк Вілі

39
Це гірше, ніж "на кілька хвилин" - ви НІКОЛИ не бачите. Відповідно до моделі пам'яті, JVM дозволяється оптимізувати час (! Зупинка) у той час, коли (справжнє), а потім ви шлангуєте. Це може статися лише на деяких віртуальних машинах, лише в серверному режимі, лише коли JVM перекомпілюється після x ітерацій циклу тощо. Так!
Коуан

2
Чому ви хочете використовувати AtomicBoolean над летючими булевими? Я розробляю версію 1.4+, тож чи існують якісь підводні камені, де тільки декларується мінливість?
Басейн

2
Нік, я думаю, це тому, що атомний CAS зазвичай навіть швидший, ніж мінливий. Якщо ви розробляєте для 1.4, ваш єдиний безпечний варіант IMHO - це використовувати синхронізовану як непостійну в 1.4, не отримав сильних гарантій бар'єру пам’яті, як це має у Java 5.
Kutzi

5
@Thomas: це через модель пам'яті Java. Ви повинні прочитати про це, якщо ви хочете це детально знати (Java Concurrency in Practice by Brian Goetz пояснює це добре, наприклад). Коротше кажучи: якщо ви не використовуєте ключові слова / конструкції синхронізації пам'яті (наприклад, летючі, синхронізовані, AtomicXyz, але також коли нитка закінчена), одна тема не має жодної гарантії бачити зміни, внесені до будь-якого поля іншим потоком
Kutzi

179

Моя найболючіша проблема одночасності, коли дві різні бібліотеки з відкритим кодом робили щось подібне:

private static final String LOCK = "LOCK";  // use matching strings 
                                            // in two different libraries

public doSomestuff() {
   synchronized(LOCK) {
       this.work();
   }
}

На перший погляд, це виглядає як досить тривіальний приклад синхронізації. Однак; оскільки струни інтерновані в Java, буквальна рядок "LOCK"виявляється тим самим екземпляром java.lang.String(навіть якщо вони оголошені абсолютно неоднаково один від одного.) Результат, очевидно, поганий.


63
Це одна з причин, чому я віддаю перевагу приватному статичному кінцевому Object LOCK = new Object ();
Анджей Дойл

17
Мені подобається - о, це противно :)
Thorbjørn Ravn Andersen

7
Це добре для Java Puzzlers 2.
Dov Wasserman

12
Насправді ... мені дуже хочеться, щоб компілятор відмовився дозволяти синхронізувати на String. З огляду на String interning, не існує жодного випадку, коли це було б «доброю справою (tm)».
Джаред

3
@Jared: "поки рядок не інтернується" не має сенсу. Рядки не магічно "стають" інтернованими. String.intern () повертає інший об'єкт, якщо ви вже не маєте канонічний екземпляр зазначеної String. Крім того, всі буквальні рядки та постійні вирази, що оцінюються за рядками, інтерновані. Завжди. Див. Документи для String.intern () та §3.10.5 JLS.
Лоранс Гонсальвс

65

Одна класична проблема - зміна об'єкта, на якому ви синхронізуєтесь, синхронізуючи його:

synchronized(foo) {
  foo = ...
}

Інші одночасні потоки синхронізуються з іншим об'єктом, і цей блок не забезпечує взаємного виключення, яке ви очікуєте.


19
Для цього існує перевірка IDEA під назвою "Синхронізація на не завершальному полі, навряд чи матиме корисну семантику". Дуже хороша.
Jen S.

8
Ха ... тепер це замучений опис. "навряд чи матиме корисну семантику" можна було б краще описати як "найімовірніше, зламана". :)
Алекс Міллер

Я думаю, що це була гірша Java, яка мала це у своєму ReadWriteLock. На щастя, зараз у нас є java.util.concurrency.locks, а Даг трохи більше на кулі.
Том Хотін - тайклін

Я також часто бачив цю проблему. Синхронізація лише на кінцевих об'єктах. FindBugs та ін. допомога, так.
gimpf

це лише проблема під час присвоєння? (див. наведений нижче приклад @Alex Miller з картою) Чи буде в цьому прикладі карти також ця проблема?
Алекс Бердслі

50

Поширеною проблемою є використання таких класів, як Calendar та SimpleDateFormat з декількох потоків (часто кешуючи їх у статичній змінній) без синхронізації. Ці класи не є безпечними для потоків, тому багатопотоковий доступ в кінцевому підсумку спричинить дивні проблеми з непослідовним станом.


Чи знаєте ви про будь-який проект з відкритим кодом, що містить цю помилку в якійсь її версії? Я шукаю конкретні приклади цієї помилки в реальному програмному забезпеченні.
перепрограміст

48

Не належна синхронізація з об’єктами, поверненими Collections.synchronizedXXX(), особливо під час ітерації чи декількох операцій:

Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

...

if(!map.containsKey("foo"))
    map.put("foo", "bar");

Це неправильно . Незважаючи synchronizedна одночасні операції , стан карти між викликом containsі putможе бути змінений іншим потоком. Вона повинна бути:

synchronized(map) {
    if(!map.containsKey("foo"))
        map.put("foo", "bar");
}

Або з ConcurrentMapреалізацією:

map.putIfAbsent("foo", "bar");

6
Або ще краще, використовуйте ConcurrentHashMap і putIfAbsent.
Том Хотін - тайклін

47

Двічі перевірено блокування. За великим рахунком.

Парадигма, за якою я почав вивчати проблеми, коли працював у BEA, полягає в тому, що люди перевірятимуть одиночку таким чином:

public Class MySingleton {
  private static MySingleton s_instance;
  public static MySingleton getInstance() {
    if(s_instance == null) {
      synchronized(MySingleton.class) { s_instance = new MySingleton(); }
    }
    return s_instance;
  }
}

Це ніколи не працює, тому що інший потік, можливо, потрапив до синхронізованого блоку, і s_instance вже не є нульовою. Тож природна зміна полягає в тому, щоб це зробити:

  public static MySingleton getInstance() {
    if(s_instance == null) {
      synchronized(MySingleton.class) {
        if(s_instance == null) s_instance = new MySingleton();
      }
    }
    return s_instance;
  }

Це не працює, тому що модель пам'яті Java не підтримує її. Вам потрібно оголосити s_instance як мінливу, щоб вона працювала, і навіть тоді вона працює лише на Java 5.

Люди, які не знайомі з тонкощами моделі пам'яті Java, псують це весь час .


7
Однотонний малюнок вирішує всі ці проблеми (див. Коментарі до цього Джоша Блоха). Знання про його існування мають бути більш поширеними серед Java-програмістів.
Робін

Мені ще належить наткнутися на один випадок, коли ледача ініціалізація сингтона була дійсно доречною. А якщо це так, просто оголосимо метод синхронізованим.
Дов Васерман

3
Це я використовую для ледачої ініціалізації класів Singleton. Також не потрібно синхронізації, оскільки це гарантовано явним чином. клас Foo {держатель статичного класу {статичний Foo foo = новий Foo (); } статичний Foo getInstance () {повернути Holder.foo; }}
Ірфан Зульфікар

Ірфан, так називається метод П'ю, з чого я пам’ятаю
Кріс Р,

@Robin, чи не простіше просто використовувати статичний ініціалізатор? Вони завжди гарантовано працюють синхронізовано.
мат б

37

Хоча, мабуть, не саме те, про що ви просите, найчастіша проблема, пов’язана з одночасністю, з якою я стикався (можливо, тому, що вона з'являється у звичайному однопотоковому коді) - це

java.util.ConcurrentModificationException

викликані такими речами, як:

List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String string : list) { list.remove(string); }

Ні, це абсолютно те, що я шукаю. Дякую!
Алекс Міллер

30

Думаю, що синхронізовані колекції дають вам більший захист, ніж вони насправді, і забути утримувати замок між дзвінками. Я кілька разів бачив цю помилку:

 List<String> l = Collections.synchronizedList(new ArrayList<String>());
 String[] s = l.toArray(new String[l.size()]);

Наприклад, у другому рядку вище toArray()і size()обидва способи є безпечними для потоку, але значення size()оцінюється окремо відtoArray() , а блокування у списку не проводиться між цими двома викликами.

Якщо ви запускаєте цей код за допомогою іншого потоку, одночасно видаляючи елементи зі списку, рано чи пізно у вас з’явиться новий String[]повернутий, який більший, ніж потрібно для вміщення всіх елементів у списку, і має нульові значення в хвості. Неважко подумати, що оскільки два виклики методу до Списку відбуваються в одному рядку коду, це якимось чином атомна операція, але це не так.


5
хороший приклад. Я думаю, що я б це пообіцяв більше, оскільки "склад атомних операцій не є атомним". (Дивіться мінливе поле ++ для іншого простого прикладу)
Алекс Міллер

29

Найпоширеніша помилка, яку ми бачимо, де я працюю, - це програмісти, які виконують довгі операції, як-от виклики сервера, на EDT, блокуючи GUI на кілька секунд і роблячи додаток невідповідним.


за одну з таких відповідей я хочу, щоб я міг дати більше, ніж один бал
Epaga

2
EDT = Тема відправки події
mjj1409

28

Забувши зачекати () (або Condition.await ()) у циклі, перевіривши, чи справді умова очікування справдиться. Без цього ви стикаєтеся з помилками через помилкові пробудження wait (). Канонічне використання повинно бути:

 synchronized (obj) {
     while (<condition does not hold>) {
         obj.wait();
     }
     // do stuff based on condition being true
 }

26

Ще одна поширена помилка - це погана обробка виключень. Коли фоновий потік видає виняток, якщо ви не обробляєте його належним чином, ви можете взагалі не побачити сліду стека. Або, можливо, ваше фонове завдання перестає виконуватись і ніколи не запускається заново, оскільки ви не впоралися з винятком.


Так, і є хороші інструменти для вирішення цього питання зараз з обробниками.
Алекс Міллер

3
Чи можете ви розмістити посилання на будь-які статті чи посилання, які пояснюють це більш детально?
Абхієет Кашня

22

Поки я не брав заняття з Брайаном Гетцем, я не розумів, що несинхронізоване getterприватне поле, мутоване за допомогою синхронізованого setter, ніколи не гарантує повернення оновленого значення. Тільки тоді, коли змінна захищена синхронізованим блоком на обох прочитаних І записаних , ви отримаєте гарантію останнього значення змінної.

public class SomeClass{
    private Integer thing = 1;

    public synchronized void setThing(Integer thing)
        this.thing = thing;
    }

    /**
     * This may return 1 forever and ever no matter what is set
     * because the read is not synched
     */
    public Integer getThing(){
        return thing;  
    }
}

5
У пізніших JVM (я думаю, що 1,5 і вперед), використання летючого виправить це також.
Джеймс Шек

2
Не обов'язково. непостійний дає вам останнє значення, тому запобігає поверненню 1 назавжди, але не забезпечує блокування. Його близько, але не зовсім те саме.
Джон Рассел

1
@JohnRussell Я думав, що мінливість гарантує взаємини, що трапляються раніше. Хіба це не "блокування"? "Запис у мінливу змінну (§ 8.3.1.4) v синхронізується - з усіма наступними зчитуваннями v будь-яким потоком (де наступне визначається відповідно до порядку синхронізації)."
Шон

15

Думаючи, що ви пишете однопотоковий код, але використовуєте змінну статику (включаючи одинакові). Очевидно, вони будуть розділені між потоками. Це трапляється напрочуд часто.


3
Так, справді! Змінна статика порушує обмеження ниток. Дивно, але я ніколи не знаходив нічого про цей підводний камінь ні в JCiP, ні в CPJ.
Жульєн Частанг

Я би сподівався, що це очевидно для людей, які займаються одночасним програмуванням. Глобальна держава повинна стати першим місцем для перевірки безпеки потоків.
gtrak

1
@Gary Thing, вони не думають, що вони роблять одночасне програмування.
Том Хотін - тайклін

15

Виклики довільних методів не повинні здійснюватися із синхронізованих блоків.

Дейв Рей торкнувся цього у своїй першій відповіді, і насправді я також зіткнувся з тупиком, що також стосується виклику методів для слухачів із синхронізованого методу. Я думаю, що більш загальним уроком є ​​те, що виклики методів не слід робити "в дику природу" зсередини синхронізованого блоку - ви не маєте уявлення, чи буде виклик довготривалим, чи призведе до тупикової ситуації чи будь-чого іншого.

У цьому випадку і, як правило, загалом, рішення полягало в тому, щоб зменшити область застосування синхронізованого блоку, щоб просто захистити критичну приватний розділ коду.

Крім того, оскільки ми тепер отримували доступ до Колекції слухачів поза синхронізованого блоку, ми змінили це на Збірник копіювання за записом. Або ми могли просто зробити захисну копію колекції. Справа в тому, що зазвичай є альтернативи безпечному доступу до Колекції невідомих об'єктів.


13

Остання помилка, пов’язана з Concurrency, на яку я зіткнувся, - це об’єкт, який у своєму конструкторі створив ExecutorService, але коли на об'єкт більше не було посилань, він ніколи не вимикав ExecutorService. Таким чином, протягом тижня тисячі ниток просочилися, врешті-решт спричиняючи крах системи. (Технічно він не вийшов з ладу, але перестав функціонувати належним чином, продовжуючи працювати.)

Технічно я вважаю, що це не проблема паралельності, але це проблема, що стосується використання бібліотек java.util.concurrency.


11

Збалансована синхронізація, особливо щодо Карт, здається досить поширеною проблемою. Багато людей вважають, що синхронізація на картах (не ConcurrentMap, але кажуть HashMap), а не синхронізація на get є достатньою. Це, однак, може призвести до нескінченного циклу під час повторного хешування.

Ця ж проблема (часткова синхронізація) може виникнути в будь-якому місці, де ви поділилися станом, але читає і записує.


11

У сервлетів я зіткнувся з проблемою одночасності, коли є змінні поля, які встановлюватимуться кожним запитом. Але є лише один екземпляр сервлета для всіх запитів, тож це прекрасно працювало в одному середовищі користувача, але коли більше одного користувача просили сервлет виникнути непередбачувані результати.

public class MyServlet implements Servlet{
    private Object something;

    public void service(ServletRequest request, ServletResponse response)
        throws ServletException, IOException{
        this.something = request.getAttribute("something");
        doSomething();
    }

    private void doSomething(){
        this.something ...
    }
}

10

Не зовсім помилка, але найгіршим гріхом є надання бібліотеки, яку ви збираєтесь використовувати інші люди, але не вказуючи, які класи / методи є безпечними для потоків та які потрібно викликати лише з однієї нитки тощо.

Більше людей повинні використовувати анотації про сумісність (наприклад, @ThreadSafe, @GuardedBy тощо), описані в книзі Геца.


9

Моя найбільша проблема завжди були тупиками, особливо це викликано слухачами, які звільняються із замком. У цих випадках отримати перевернутий замок між двома нитками дуже просто. У моєму випадку між моделюванням, що працює в одному потоці, і візуалізацією моделювання, що працює в потоці інтерфейсу користувача.

РЕДАКТУВАННЯ: Друга частина перенесена на окрему відповідь


Чи можете ви розділити останній на окрему відповідь? Давайте збережемо його 1 за допис. Це два справді хороших.
Алекс Міллер

9

Запустити потік у конструкторі класу проблематично. Якщо клас розширений, потік можна запустити перед виконанням конструктора підкласу .


8

Класи, що змінюються, в спільних структурах даних

Thread1:
    Person p = new Person("John");
    sharedMap.put("Key", p);
    assert(p.getName().equals("John");  // sometimes passes, sometimes fails

Thread2:
    Person p = sharedMap.get("Key");
    p.setName("Alfonso");

Коли це відбувається, код набагато складніший, ніж цей спрощений приклад. Повторити, знайти та виправити помилку важко. Можливо, цього можна було б уникнути, якби ми могли позначити певні класи як непорушні, а певні структури даних - лише ті, що тримають незмінні об’єкти.


8

Синхронізація на рядковому буквальному або константному, визначеному літеральним рядком, є (потенційно) проблемою, оскільки інтерламент рядка є інтернованим і буде спільним для всіх інших в JVM, використовуючи той самий літеральний рядок. Я знаю, що ця проблема виникла на серверах додатків та інших «контейнерних» сценаріях.

Приклад:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

У цьому випадку кожен, хто використовує рядок "foo" для блокування, ділиться тим самим замком.


Потенційно це заблоковано. Проблема полягає в тому, що семантика інтервалів WHEN рядків не визначена (або, IMNSHO, недостатньо визначена). Постійна час компілятора "foo" інтернірується, "foo", що надходить з мережевого інтерфейсу, інтернується лише тоді, коли ви це зробите.
Кірк Вілі

Так, саме тому я спеціально використовував буквальну струнну константу, яка гарантовано буде інтернованою.
Алекс Міллер

8

Я вірю, що в майбутньому основною проблемою Java буде гарантія видимості для конструкторів (відсутність). Наприклад, якщо ви створюєте наступний клас

class MyClass {
    public int a = 1;
}

а потім просто прочитайте властивість MyClass a з іншого потоку, MyClass.a може бути 0 або 1, залежно від реалізації JavaVM та настрою. Сьогодні шанси на «а» бути 1 дуже високі. Але на майбутніх машинах NUMA це може бути інакше. Багато людей цього не знають і вважають, що їм не потрібно дбати про багатопотоковість на етапі ініціалізації.


Я вважаю це м'яко дивовижним, але я знаю, що ти розумний чувак Тім, тому я прийму його з посиланням. :) Однак, якби фінал був остаточним, це не було б проблемою, правда? Тоді ви будете пов'язані остаточною семантикою заморожування під час будівництва?
Алекс Міллер

Я все ще знаходжу в СУМ речі, які мене здивують, тому я б мені не довіряв, але я майже впевнений у цьому. Дивіться також cs.umd.edu/~pugh/java/memoryModel/… . Якщо поле було остаточним, це не було б проблемою, воно було б видно після фази ініціалізації.
Тім Янсен

2
Це лише проблема, якщо посилання на щойно створений екземпляр використовується вже до повернення / завершення конструктора. Наприклад, клас реєструється під час побудови в публічному пулі та інші потоки починають отримувати доступ до нього.
ReneS

3
MyClass.a вказує на статичний доступ, а "a" не є статичним членом MyClass. Окрім цього, це так, як зазначено у «ReneS», це лише проблема, якщо витікає посилання на незавершений об’єкт, як, наприклад, додавання «цього» до якоїсь зовнішньої карти в конструкторі.
Маркус Джевринг

7

Найбільш тупа помилка, яку я часто роблю, - це забути синхронізуватись перед тим, як викликати сповіщення () або чекати () на об’єкті.


8
На відміну від більшості проблем з одночасністю, чи не легко це знайти? Принаймні, ви отримуєте тут IllegalMonitorStateException ...
Програматор поза законом

На щастя, це дуже легко знайти ... але це все-таки німа помилка, яка витрачає мій час більше, ніж слід :)
Дейв Рей

7

Використання локального "нового об'єкта ()" як mutex.

synchronized (new Object())
{
    System.out.println("sdfs");
}

Це марно.


2
Це, мабуть, марно, але акт синхронізації взагалі робить щось цікаве ... Безумовно, щоразу створювати новий Об'єкт - це повна марна трата.
ДРЕВО

4
Це не марно. Це бар'єр пам’яті без блокування.
Девід Руссель

1
@David: єдина проблема - jvm міг оптимізувати її, видаливши такий замок взагалі
yetagethercoder

@insighter Я бачу, що ваша думка поділяється ibm.com/developerworks/java/library/j-jtp10185/index.html Я погоджуюся, що це робити дурною справою, оскільки ви не знаєте, коли ваш бар'єр пам’яті синхронізується, я просто вказував, що робить більше, ніж нічого.
Девід Руссель

7

Інша поширена проблема "одночасності" - використовувати синхронізований код, коли це зовсім не потрібно. Наприклад, я все ще бачу програмістів, які використовують StringBufferабо навіть java.util.Vector(як локальні змінні).


1
Це не є проблемою, але зайвою, оскільки вона повідомляє JVM синхронізувати дані проти глобальної пам’яті, і тому може працювати погано на мульти-процесорі, навіть ніхто не використовує блок синхронізації одночасно.
ReneS

6

Кілька об'єктів, які захищені замком, але зазвичай отримують доступ послідовно. Ми натрапили на кілька випадків, коли блокування отримуються за різним кодом у різних порядках, що призводить до тупикової ситуації.


5

Не розуміючи, що thisвнутрішній клас не thisє зовнішнім класом. Зазвичай в анонімному внутрішньому класі, який реалізує Runnable. Основна проблема полягає в тому, що оскільки синхронізація є частиною всіх Objects, фактично не існує перевірки статичного типу. Я бачив це щонайменше двічі на Usenet, а також з'являється у програмі Brian Goetz'z Java Concurrency in Practice.

Закриття BGGA не страждає від цього, оскільки немає thisдля закриття ( thisпосилається на зовнішній клас). Якщо ви використовуєте необ'єкти thisяк блоки, то це може вирішити цю проблему та інші.


3

Використання глобального об'єкта, такого як статична змінна для блокування.

Це призводить до дуже поганих результатів через суперечки.


Ну, іноді, іноді ні. Якби це було просто так ...
gimpf

Якщо припустити, що нарізка допомагає взагалі підвищити продуктивність для заданої проблеми, вона завжди погіршує продуктивність, як тільки більше одного потоку отримує доступ до коду, захищеного замком.
kohlerm

3

Чесно? До появи java.util.concurrentнайпоширенішої проблеми, з якою я звичайно стикався, було те, що я називаю "обмолотом ниток": програми, які використовують нитки для одночасності, але створюють занадто багато їх і закінчують обробляти.


Ви маєте на увазі, що у вас виникли нові проблеми тепер, коли java.util.concurrent доступний?
Анджей Дойл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.