0

Это своего рода Java Puzzler, который я наткнулся и не могу объяснить. Может, кто-нибудь может?Java Puzzler: занят ждать, когда потоки перестают работать

Следующая программа зависает через короткое время. Иногда после двух выходов, иногда после 80, но почти всегда до правильного завершения. Возможно, вам придется запустить его несколько раз, если это не произойдет в первый раз.

public class Main { 
    public static void main(String[] args) { 
     final WorkerThread[] threads = new WorkerThread[]{ new WorkerThread("Ping!"), new WorkerThread("Pong!") }; 
     threads[0].start(); 
     threads[1].start(); 

     Runnable work = new Runnable() { 
      private int counter = 0; 
      public void run() { 
       System.out.println(counter + " : " + Thread.currentThread().getName()); 
       threads[counter++ % 2].setWork(this); 
       if (counter == 100) { 
        System.exit(1); 
       } 
      } 
     }; 

     work.run(); 
    } 
} 

class WorkerThread extends Thread { 
    private Runnable workToDo; 

    public WorkerThread(String name) { 
     super(name); 
    } 

    @Override 
    public void run() { 
     while (true){ 
      if (workToDo != null) { 
       workToDo.run(); 
       workToDo = null; 
      } 
     } 
    } 

    public void setWork(Runnable newWork) { 
     this.workToDo = newWork; 
    } 
} 

Теперь ясно, что занятые петли ожидания - отличная идея в целом. Но это не об улучшении, а о понимании происходящего.

Поскольку все работает, как ожидается, когда WorkerThread.setWork() является synchronized или когда WorkerThread.workToDo поле установлено volatile Я подозреваю проблему памяти.

Но почему именно это происходит? Отладка не помогает, как только вы начинаете шаг за шагом, все ведет себя так, как ожидалось.

Объяснение будет оценено.

ответ

1

Проблема возникает прямо между этими линиями:

workToDo.run(); 
workToDo = null; 

Пусть следующая последовательность событий:

- Original Runnable runs. "Ping!".setWork() called 
- Ping! thread realizes workToDo != null, calls run(), the stops between those two lines 
    - "Pong!".setWork() called 
- Pong! thread realizes workToDo != null, calls run() 
    - "Ping!".setWork() called 
- Ping! thread resumes, sets workToDo = null, ignorantly discarding the new value 
- Both threads now have workToDo = null, and the counter is frozen at 2,...,80 
    Program hangs 
+0

Я думаю, что здесь также существует проблема кеш-когерентности. Создание 'workToDo'' volatile' или создание 'setWork' для' synchronized' будет исправлять когерентность кеша, заставляя значение 'workToDo' быть видимым для всех потоков. Тем не менее, это не решит условие гонки, которое вы определили, для чего потребуется блокировка или другая конструкция, чтобы предотвратить вызов другого потока из 'setWork()', в то время как этот поток находится между 'workToDo.run()' и 'workToDo = null '. –

2
  1. Первая проблема заключается в том, что вы устанавливаете Runnable workToDo от main нити, а затем читает его в 2 раздвоенными нитями без синхронизации. Каждый раз, когда вы изменяете поле в нескольких потоках, оно должно быть помечено как volatile или кому-то synchronized.

    private volatile Runnable workToDo; 
    
  2. Кроме того, поскольку несколько потоков делают counter++ это также должно быть synchronized. Я рекомендую для этого AtomicInteger.

    private AtomicInteger counter = new AtomicInteger(0); 
    ... 
    threads[counter.incrementAndGet() % 2].setWork(this); 
    
  3. Но я думаю, что реальная проблема может быть одним из условий гонки. Для обоих потоков можно установить workToDo как Runnable, а затем вернуть их и вернуть их обратно null, чтобы они просто вращались навсегда. Я не знаю, как это исправить.

    1. threads[0] has it's `workToDo` set to the runnable. It calls `run()`. 
    2. at the same time threads[1] also calls `run()`. 
    3. threads[0] sets the `workToDo` on itself and threads[1] to be the runnable. 
    4. at the same time threads[1] does the same thing. 
    5. threads[0] returns from the `run()` method and sets `workToDo` to be `null`. 
    6. threads[1] returns from the `run()` method and sets `workToDo` to be `null`. 
    7. They spin forever... 
    

И, как вы говорите, петля спина с ума, но я предполагаю, что это программа демонстрации нити.

+2

Использование услуг Монитор объектов (wait/notify) также будет полезен – MadProgrammer

+0

Да, @MadProgrammer. Я предполагаю, что это какая-то демо-программа нити или что-то в этом роде. – Gray

+0

Часть «должно» понятна, как описано выше, это устраняет проблему. Но почему? Как-то содержимое поля, кажется, теряется, и оба потока остаются с полем «null» (следовательно, ожидание - навсегда).Однако, когда вы устанавливаете точку останова в то время в методе 'run()' WorkerThread, текущий поток останавливается и неожиданно видит 'workToDo' снова. Я ищу более подробное объяснение. – cheppsn

0

мои 2 цента ....

import java.util.concurrent.atomic.AtomicReference; 

class WorkerThread extends Thread { 
    private AtomicReference<Runnable> work; 

    public WorkerThread(String name) { 
     super(name); 
     work = new AtomicReference<Runnable>(); 
    } 

    @Override 
    public void run() { 
     while (true){ 
      Runnable workToDo = work.getAndSet(null); 
      if (workToDo != null) { 
       workToDo.run(); 
      } 
     } 
    } 

    public void setWork(Runnable newWork) { 
     this.work.set(newWork); 
    } 
} 
+0

Это решает условие гонки, все еще существует вероятность того, что setWork будет вызван до того, как рабочий элемент будет обслуживаться. работа должна, вероятно, быть в очереди, или setWork должен блокироваться до тех пор, пока работа не будет очищена, прежде чем разрешить ее установку. – Kevin

Смежные вопросы