2016-07-03 16 views
3

Итак, вот код. В принципе, если мы изменим классы ReadCalculation и Calculator для расширения Thread вместо внедрения Runnable, нам нужно будет создать эти классы и передать их в новый объект потока или просто вызвать start() на них.Различное поведение при реализации Runnable вместо продолжения Thread

Calculator calc = new Calculator(); 
new ReadCalculation(calc).start(); 
new ReadCalculation(calc).start(); 
calc.start(); 

Ничего особенного до сих пор .. Но когда вы выполняете эту крошечную программу, есть огромный шанс, что ваши потоки будут оставаться заблокированными «Ожидание расчета ...» если мы будем над Runnable реализации над простирающимся класс Thread.

Если мы расширим класс Thread вместо реализации Runnable, поведение будет правильным без каких-либо признаков состояния гонки. Любые идеи, по которым может быть источником такого поведения?

public class NotifyAllAndWait { 

public static void main(String[] args) { 

     Calculator calc = new Calculator(); 
     Thread th01 = new Thread(new ReadCalculation(calc)); 
     th01.start(); 
     Thread th02 = new Thread(new ReadCalculation(calc)); 
     th02.start(); 

     Thread calcThread = new Thread(calc); 
     calcThread.start(); 
    } 
} 

class ReadCalculation implements Runnable { 

    private Calculator calc = null; 
    ReadCalculation(Calculator calc) { 
     this.calc = calc; 
    } 

    @Override 
    public void run() { 
     synchronized (calc) { 
      try { 
       System.out.println(Thread.currentThread().getName() + " Waiting for calculation..."); 
       calc.wait(); 
      } catch (InterruptedException e) { 
       e.printStackTrace(); 
      } 
      System.out.println(Thread.currentThread().getName() + " Total: " + calc.getTotal()); 
     } 
    } 
} 

class Calculator implements Runnable { 
    private int total = 0; 
    @Override 
    public void run() { 
     synchronized(this) { 
      System.out.println(Thread.currentThread().getName() + " RUNNING CALCULATION!"); 
      for(int i = 0; i < 100; i = i + 2){ 
       total = total + i; 
      } 
      notifyAll(); 
     } 
    } 
    public int getTotal() { 
     return total; 
    } 
} 
+0

Я не могу ссылаться на какую-либо часть языка или спецификацию JVM, которая может быть здесь воспроизведена, но наиболее вероятной практикой является то, что исходный код 'Thread' занимает очень много блокировок для обоих потоков. class' и текущий экземпляр 'Thread'. Вы блокируете экземпляр калькулятора как внутри, так и снаружи 'calc'. Эти блокировки могут не мешать блокировке внутри 'Thread', когда вы используете выделенную целевую Runnable (только что проверено: ни один код в реализации Android Thread не блокирует переданный Runnable), но, безусловно, повлияет на порядок выполнения при расширении Thread. – user1643723

ответ

2

В версии implements Runnable, по крайней мере, вы ничего не делаете для того, чтобы ReadCalculation потоки достигают wait() до Calculator нить входит в свой synchronized блок. Если нить Calculator сначала входит в блок synchronized, тогда он будет вызывать notifyAll() до того, как потоки ReadCalculation вызвали wait(). И если это произойдет, то notifyAll() будет no-op, а потоки ReadCalculation будут ждать навсегда. (Это происходит потому, что notifyAll() заботится только о потоках, которые уже ожидания на объекте, он делает не установить какой-либо индикатор на объекте, которые могут быть обнаружены при последующих вызовах wait().)

Чтобы исправить вы можете добавить свойство Calculator, которые могут быть использованы для проверки проделанной Несс, и вызывать только wait() если Calculator является не сделано:

if(! calc.isCalculationDone()) { 
    calc.wait(); 
} 

(Обратите внимание, что для того, чтобы полностью избежать состояние гонки, очень важно, чтобы вся if -statement быть внутриsynchronized блока, и что Calculator установить это свойство внутриsynchronized блока, который вызывает notifyAll(). Понимаете ли вы, почему?)

(Кстати, комментарий Питера Лоури о том, что «поток может легко выполнить ваши 100 итераций до начала других потоков», очень вводит в заблуждение, поскольку в вашей программе происходит 100 итераций послеCalculator ввел свой блок synchronized.Так как потоки ReadCalculation блокируются от ввода их блоков и вызывают calc.wait(), а Calculator - в своем блоке synchronized, не имеет значения, является ли это 1 итерация, 100 итераций или 1 000 000 итераций, если у нее нет интересных эффектов оптимизации, которые могли бы изменить сроки программы до того, что точка.)


Вы не выложили всю extends Thread версии, но если я правильно понимаю, что это выглядит, то это на самом деле все еще имеет такое же состояние гонки. Тем не менее, в природе условий гонки незначительные изменения могут значительно повлиять на вероятность неправильного поведения. Вам по-прежнему необходимо исправить состояние гонки, даже если оно, по-видимому, действительно плохо себя ведет, потому что почти наверняка, что это будет неправильно, если вы запустите программу достаточно раз.

У меня нет хорошего объяснения, почему неправильное поведение происходит гораздо чаще с одним подходом, чем с другим; но, как и комментарии пользователя1643723 выше, подход extends Thread подразумевает, что множество кода других, кроме вашего, также может заблокировать ваш экземпляр Calculator; и это может иметь какой-то эффект. Но, честно говоря, я не думаю, что слишком много стоит беспокоиться о причинах, по которым гоночное положение может часто приводить к неправильному поведению; мы должны исправить это независимо, так, конец истории.


Кстати:

  • Выше я использовал if(! calc.isCalculationDone()); но на самом деле лучше всего всегда перематывать звонки на wait() в подходящий while -loop, так что действительно вы должны написать . Есть две основные причины для этого:

    • В нетривиальных программ, вам не обязательно знать, почему notifyAll() был назван, или даже если вы делаете, вы не знаете ли, что причина по-прежнему применяется на время, когда ожидающая нить действительно проснулась и восстановила synchronized -lock. Это значительно облегчает рассуждение о правильности ваших взаимодействий, если вы используете структуру while(not_ready_to_proceed()) { wait(); }, чтобы выразить идею wait_until_ready_to_proceed(), вместо того, чтобы просто писать wait() и пытаться убедиться, что ничто никогда не приведет к ее возврату, не готов.

    • В некоторых операционных системах отправка сигнала процессу пробуждает все потоки wait() -ing. Это называется ложным пробуждением; см. "Do spurious wakeups actually happen?" для получения дополнительной информации. Таким образом, поток может проснуться, даже если ни один другой поток не называется notify() или notifyAll().

  • for -loop в Calculator.run() не должно быть в synchronized блоке, так как она не требует какой-либо синхронизации, так что утверждение не требуется. В вашей небольшой программе это на самом деле не имеет никакого значения (поскольку ни один из других потоков на самом деле не имеет ничего общего с этой точкой), но наилучшей практикой всегда является попытка свести к минимуму количество кода внутри блоков synchronized.

2

При выполнении wait() это должно быть в цикле после изменения состояния вы проводившая notify() блока. например

// when notify 
changed = true; 
x.notifyAll(); 

// when waiting 
while(!changed) 
    x.wait(); 

Если вы не сделаете это, вы столкнетесь с вопросом, такие как ожидание() пробуждении поддельно или уведомить() утрачивается.

Примечание: поток может легко выполнить до 100 повторений до того, как другие потоки начнут работать. Возможно, что предварительное создание объекта Thread делает достаточно разницу в производительности для изменения результата в вашем случае.

+1

Как бы то ни было, ваше первое утверждение выглядит неактуальным. – user1643723

+0

@ user1643723 рад быть исправлен с лучшим ответом, который объясняет проблему OP. –

+0

@PeterLawrey заботится о том, чтобы детализировать «оповещать о пропаже»? [Глава JLS об уведомлении()] (https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.2.2) не упоминает ничего подобного. – user1643723