Как потоки совместно используют переменную поля того же экземпляра, который их создает?

Я хочу протестировать интерфейс Runnable. Создайте экземпляр класса, реализующего интерфейс Runnable. А затем создайте три потока с помощью одного и того же экземпляра. Обратите внимание на то, как потоки совместно используют переменную поля экземпляра. Два вопроса: 1. Почему два результата не похожи на последовательность «20, 19, 18 .... 1, 0»? 2. Почему два результата отличаются друг от друга? (Я запускаю код дважды.) Код выглядит следующим образом:

public class ThreadDemo2 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        TestThread tt = new TestThread();
        Thread t1 = new Thread(tt);
        Thread t2 = new Thread(tt);
        Thread t3 = new Thread(tt);
        t1.start();
        t2.start();
        t3.start();
    }
}
class TestThread implements Runnable {
    public int tickets = 20;
    public void run(){
        while (tickets >= 0){
            System.out.println(Thread.currentThread().getName() + ":the number of tickets is " + tickets--);
        }
    }
}

Я запускаю код дважды. Два результата показаны ниже. В первый раз:

Thread-1:the number of tickets is 20
Thread-2:the number of tickets is 18
Thread-2:the number of tickets is 16
Thread-0:the number of tickets is 19
Thread-2:the number of tickets is 15
Thread-1:the number of tickets is 17
Thread-2:the number of tickets is 13
Thread-0:the number of tickets is 14
Thread-2:the number of tickets is 11
Thread-1:the number of tickets is 12
Thread-2:the number of tickets is 9
Thread-0:the number of tickets is 10
Thread-2:the number of tickets is 7
Thread-1:the number of tickets is 8
Thread-2:the number of tickets is 5
Thread-0:the number of tickets is 6
Thread-2:the number of tickets is 3
Thread-1:the number of tickets is 4
Thread-2:the number of tickets is 1
Thread-0:the number of tickets is 2
Thread-1:the number of tickets is 0

Второй раз:

Thread-0:the number of tickets is 19
Thread-2:the number of tickets is 18
Thread-2:the number of tickets is 16
Thread-2:the number of tickets is 15
Thread-1:the number of tickets is 20
Thread-2:the number of tickets is 14
Thread-2:the number of tickets is 12
Thread-2:the number of tickets is 11
Thread-0:the number of tickets is 17
Thread-2:the number of tickets is 10
Thread-2:the number of tickets is 8
Thread-1:the number of tickets is 13
Thread-1:the number of tickets is 6
Thread-1:the number of tickets is 5
Thread-2:the number of tickets is 7
Thread-0:the number of tickets is 9
Thread-2:the number of tickets is 3
Thread-1:the number of tickets is 4
Thread-2:the number of tickets is 1
Thread-0:the number of tickets is 2
Thread-1:the number of tickets is 0

person Ryan    schedule 19.04.2014    source источник
comment
Ваш результат может отличаться в зависимости от скорости процессора и нагрузки задачи.   -  person Hello World    schedule 20.04.2014


Ответы (4)


Добро пожаловать в удивительный мир параллельной обработки. При использовании потоков никто не может гарантировать, как будет запланирован их прогресс, если вы не используете механизм синхронизации, такой как блокировки и барьеры.

Здесь вы пытаетесь распечатать единый унифицированный выходной поток, якобы показывающий, как потоки продвигаются. Это означает, что вы объединяете распечатки из потоков, но не можете сказать, как эти распечатки чередуются. Кроме того, печать не обязательно выполняется в том порядке, в котором вызываются функции, существует несколько уровней буферизации и, что еще хуже, фактический вызов кода печати не выполняется атомарно с чтением и декрементом.

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

Когда вы видите в первом примере -

Thread-2:the number of tickets is 16
Thread-0:the number of tickets is 19

Поток 0 фактически сначала прочитал и уменьшил значение переменной, но печать была отложена (из-за переключения контекста или чего-то еще). Поток 2 был запущен после того, как несколько других экземпляров уже сделали это, но сразу же должен был распечатать свое сообщение, и только после этого поток 0 смог завершить этот более ранний экземпляр.
Обратите внимание, что здесь вы не видите, что поток 0 печатает какое-либо другое значение между ними следующая итерация уже читает 14.

Изменить: Чтобы уточнить детали, вот небольшой пример возможного чередования.

Скажем, машинный код для каждого потока - (в придуманном псевдоформате)

label:
    load [var] -> rax
    dec rax
    store rax -> [var]
    call print function // implicitly uses rax 
    cmp rax, 0
    jg label  /// jump-if-greater

(var - это место в памяти, например, в стеке)

Допустим, у вас работает 2 потока. Одно возможное чередование могло быть -

thread 0              |   thread 1
------------------------------------
load [var] -> rax     |                          // reads 20
dec rax               |
store rax -> [var]    |
                      |  load [var] -> rax       // reads 19
                      |  dec rax         
                      |  store rax -> [var]
                      |  call print function     // prints 19
                      |  cmp rax, 0           
                      |  jg label 
call print function   |                          //prints 20
cmp rax, 0            |
jg label              |

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

Также обратите внимание, что у вас может быть что-то вроде

thread 0              |   thread 1
------------------------------------
load [var] -> rax     |                          // reads 20
dec rax               |
                      |  load [var] -> rax       // reads 20 again !!!
                      |  dec rax         
                      |  store rax -> [var]
store rax -> [var]    |
...

в этом случае вы получите 20 отпечатков дважды.

person Leeor    schedule 19.04.2014
comment
Большое спасибо! Я вас не совсем понял. Но я получил некоторое представление о том, что вы говорите. Насколько я понимаю, последовательность печати может отличаться от последовательности выполняемых потоков. Я должен узнать больше о JAVA, чтобы полностью понять, что вы говорите. - person Ryan; 20.04.2014
comment
@Ryan, это на самом деле не связано с Java, машинный код, который выполняется в каждом потоке, может быть вытеснен в любое время (или, если он работает на параллельных ядрах / потоках HW - он может прогрессировать с разной скоростью по разным причинам). Попробую добавить иллюстрацию. - person Leeor; 20.04.2014
comment
Я внимательно прочитал описание. Думаю, теперь я понимаю, что вы говорите, хотя я не знаю машинного кода. Большое спасибо! - person Ryan; 20.04.2014

это нормальное поведение многопоточной программы. Только ограниченное количество потоков может получить доступ к ЦП за один раз, в зависимости от мощности процессора. в многопоточной среде каждый поток получает время процессора, и этот порядок может быть или не быть последовательным.

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

вот несколько строк из полного справочника Герберта Шильдта

Java разработан для работы в широком диапазоне сред. Некоторые из этих сред реализуют многозадачность принципиально иначе, чем другие. В целях безопасности потоки с одинаковым приоритетом должны время от времени передавать управление. Это гарантирует, что все потоки смогут работать в операционной системе без вытеснения. На практике, даже в средах без вытеснения, у большинства потоков все еще есть шанс работать, потому что большинство потоков неизбежно сталкиваются с некоторыми блокирующими ситуациями, такими как ожидание ввода-вывода. Когда это происходит, заблокированный поток приостанавливается, и другие потоки могут работать. Но если вы хотите гладкого многопоточного выполнения, лучше не полагаться на это. Кроме того, некоторые типы задач интенсивно загружают процессор. Такие потоки доминируют в ЦП.

У потока может быть пять состояний. Состояние потокаКогда поток запущен, все другие потоки соревнуются за процессор. Любой поток (в соответствии со своими приоритетами) может получить процессор. Так что этот заказ не обязательно должен быть серийным. Вот почему вы получаете случайный результат при каждом запуске.

person maxx777    schedule 19.04.2014
comment
Это ответило на второй вопрос. Но почему результаты не похожи на последовательность 20, 19, 18 ... 1, 0? Независимо от того, кто получит ЦП, количество билетов должно быть последовательным, а количество потоков не может быть последовательным. - person Ryan; 20.04.2014
comment
@Ryan, это действительно хороший вопрос, и я буду вкладывать больше и больше в свой ответ. Так что следите за этим. я уже кое-что добавил сюда. - person maxx777; 20.04.2014
comment
@Ryan надеюсь, что теперь это ответит и на ваш первый вопрос - person maxx777; 20.04.2014
comment
@Ryan Как объясняет ответ Лирора: выполнение потоков чередуется. Один поток мог уменьшить счетчик, но перед записью в выходной журнал (другие потоки) тем временем могли сделать что-то еще. Этот / другие потоки могли даже уменьшить и записать новое значение до, что первый поток получит возможность записать более старое значение. Без надлежащей синхронизации это все возможные и допустимые чередования. - person Mattias Buelens; 20.04.2014
comment
@ maxx777 Большое спасибо за более подробные ответы. - person Ryan; 20.04.2014
comment
@MattiasBuelens Большое спасибо! Я получил некоторое представление об этом. - person Ryan; 20.04.2014

Итак, у вас есть три потока, работающих с одной переменной. Я предполагаю, что это задумано.

Это то, что мы называем гонкой за данные. Внутри каждого потока есть несколько операций: прочитать переменную, проверить ее, прочитать, вычесть единицу, записать обратно, распечатать. Они могут происходить в произвольном порядке между потоками. Если случится так, что двое из них вычтут из него до того, как один дойдет до его печати, то вы пропустите это число. Если один из них немного задержится перед печатью, то числа будут не в порядке.

Если вы добавите один блок synchronized (this) вокруг кода, который читает и записывает переменную, только одному потоку будет разрешено запускать этот код на tt за раз. Обязательно охватите условие цикла и печать в одном блоке, но не помещайте в блок весь цикл, иначе всю работу выполнит один поток.

while (true) 
    synchronized (this) {
        if (tickets >= 0) {
            System.out.print  (Thread.currentThread().getName());
            System.out.println(":the number of tickets is " + tickets--); 
         } else
            break;
    }
person poolie    schedule 19.04.2014
comment
Спасибо! Я пробовал код, который вы показали. Результат был в порядке. Но пробежала только одна ада. - person Ryan; 20.04.2014
comment
Запуск только одного потока является допустимой трассировкой для этой программы. Если вы хотите принудительно использовать другие потоки, вы можете немного поспать после снятия блокировки. - person poolie; 20.04.2014

Это многопоточность для вас. Потоки планируются отдельно для процессора и могут даже выполняться в реальном параллелизме на разных ядрах ЦП (если у вас многоядерный процессор). Вот почему два запуска дают разные результаты: способ планирования потоков зависит от текущей среды, которая также включает другие процессы в системе.

Выполнения потоков могут чередоваться различными способами, и порядок, в котором println из разных потоков достигает одного единственного журнала вывода, также может различаться. Один поток мог уменьшить счетчик, но перед записью в выходной журнал (другие потоки) тем временем могли сделать что-то еще. Этот / эти другие потоки могли даже уменьшить и записать новое значение до того, как первый поток получит возможность записать более старое значение. Это все возможные и допустимые чередования, и поэтому вы, вероятно, не получите убывающий порядок в своем выводе.

Черт возьми, если вы проведете еще больше тестов с большим количеством потоков, вы можете даже увидеть, как одно и то же значение появляется дважды из двух разных потоков! Это связано с тем, что два потока могут одновременно пытаться прочитать или записать поле tickets, в результате чего они оба прочитают одно и то же значение. Есть всевозможные интересные проблемы параллелизма, и именно поэтому вам нужны механизмы синхронизации, такие как блокировки, семафоры или атомарные обновления, чтобы добиться правильного поведения.

person Mattias Buelens    schedule 19.04.2014