Как использовать протокол ожидания и уведомления с несколькими потоками - PullRequest
6 голосов
/ 31 июля 2010

В частности, может кто-нибудь сказать мне, что не так с этим куском кода. Он должен запускать потоки, поэтому должен вывести «Entering thread ..» 5 раз, а затем дождаться вызова notifyAll (). Но он случайным образом печатает «Ввод ..» и «Готово ...» и продолжает ждать других.

public class ThreadTest implements Runnable {
    private int num;
    private static Object obj = new Object();
    ThreadTest(int n) {
        num=n;
    }
    @Override
    public void run() {
        synchronized (obj) {
            try {
                System.out.println("Entering thread "+num);
                obj.wait();
                System.out.println("Done Thread "+num);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }   
        }   
    }   

    public static void main(String[] args) {
        Runnable tc;
        Thread t;
        for(int i=0;i<5;i++) {
            tc = new ThreadTest(i);
            t = new Thread(tc);
            t.start();
        }
        synchronized (obj) {
            obj.notifyAll();
        }
    }
}

Ответы [ 3 ]

8 голосов
/ 31 июля 2010

Вы не делаете ничего явно неправильного с вызовами метода, но у вас есть условие гонки .

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

Таким образом, если основной поток успевает достичь синхронизированного блока раньше всего рабочегопотоки достигли вызова wait (), те рабочие потоки, которые не достигли вызова wait (), не будут работать должным образом.Поскольку текущая настройка не позволяет вам управлять этим, вы должны добавить явную обработку этого.Вы можете либо добавить какую-то переменную, которая увеличивается, когда каждый рабочий поток достигает wait (), и чтобы основной поток не вызывал notifyAll () до тех пор, пока эта переменная не достигнет 5, либо вы можете иметь цикл основного потока и многократно вызывать notifyAll (),так что рабочие потоки выпускаются в нескольких группах.

Посмотрите в пакете java.util.concurrent - есть несколько классов блокировки, которые предоставляют более мощные возможности, чем базовые синхронизированные блокировки - как всегда, Java избавляет вас от повторного использования.изобретать колесо. CountDownLatch может показаться особенно уместным.

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

2 голосов
/ 31 июля 2010

Я вторая рекомендация CountDownLatch. Вот шаблон, который я использую для своих многопоточных тестов:

final int threadCount = 200;

final CountDownLatch startPistol = new CountDownLatch(1);
final CountDownLatch startingLine = new CountDownLatch(threadCount);
final CountDownLatch finishingLine = new CountDownLatch(threadCount);

// Do a business method...
Runnable r = new Runnable() {
    public void run() {
        startingLine.countDown();
        try {
            startPistol.await();

            // TODO: challenge the multithreadedness here

        } catch (InterruptedException e) {
            Thread.interrupted();
        }
        finishingLine.countDown();
    }
};

//  -- READY --

for (int i = 0; i < threadCount; i++) {
    Thread t = new Thread(r);
    t.start();
}

// Wait for the beans to reach the finish line
startingLine.await(1000, TimeUnit.MILLISECONDS);

//  -- SET --

// TODO Assert no one has started yet

//  -- GO --

startPistol.countDown(); // go

assertTrue(finishingLine.await(5000, TimeUnit.MILLISECONDS));

//  -- DONE --

// TODO: final assert

Идея состоит в том, чтобы гарантировать, что следующая строка кода, которую будут выполнять ваши потоки, - это та, которая бросает вызов многопоточности или как можно ближе к ней. Я думаю о нитях как о бегунах на треке. Вы получаете их на дорожке (создание / запуск), ждете, пока они идеально выстроятся в линию на стартовой линии (каждый назвал startLine.countDown ()), затем стреляете из стартового пистолета (startPistol.countDown ()) и ждете всех пересечь финишную черту (отделка Line.countDown ()).

[РЕДАКТИРОВАТЬ] Следует отметить, что если у вас нет какого-либо кода или проверок, которые вы хотите выполнить между startLine.await () и startPistol.countDown (), вы можете объединить и startLine, и startPistol в один CyclicBarrier (threadCount +). 1). Подход двойного CountDownLatch по сути тот же и позволяет основному тестовому потоку выполнять любые настройки / проверки после того, как все другие потоки выстроены в очередь и перед тем, как они начнут работать, если это потребуется.

0 голосов
/ 31 июля 2010

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

Чтобы это работало (используя wait / notify), вам нужно синхронизировать основной поток, чтобы он ожидал всех дочерних потоковчтобы добраться до состояния, в котором они могут получить уведомление, прежде чем он сделает этот вызов.

Вообще говоря, выполнить синхронизацию с wait, notify и примитивными блокировками сложно.В большинстве случаев вы получите лучшие результаты (т. Е. Более простой, более надежный и более эффективный код) с помощью служебных классов параллелизма Java.

...