чи існує функція 'block до тих пір, поки умова не стане істинною' в Java?


77

Я пишу нитку слухача для сервера, і на даний момент я використовую:

while (true){
    try {
        if (condition){
            //do something
            condition=false;
        }
        sleep(1000);

    } catch (InterruptedException ex){
        Logger.getLogger(server.class.getName()).log(Level.SEVERE, null, ex);
    }
}

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

Чи існує якась функція, яка блокує, поки змінна 'умова' не стане 'істинною'? Або постійний цикл є стандартним методом очікування, поки значення змінної не зміниться?


3
Чому наведений вище код з’їдає весь ваш процесор, здається, він запускається лише раз на секунду. У будь-якому випадку побачити цю тему: stackoverflow.com/questions/289434 / ...
jontro

3
Повне висвітлення цієї теми див. У Розділі 14 “ Практика паралельної Java” . Але в більш загальному випадку , ви , ймовірно , хочете використовувати утиліти більш високого рівня , як BlockingQueue, Semaphoreабо , CountDownLatchа не механізми низького рівня.
Brian Goetz

Відповіді:


68

Такі опитування, безумовно, є найменш бажаним рішенням.

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

Основна нитка:

synchronized(syncObject) {
    try {
        // Calling wait() will block this thread until another thread
        // calls notify() on the object.
        syncObject.wait();
    } catch (InterruptedException e) {
        // Happens if someone interrupts your thread.
    }
}

Інша нитка:

// Do something
// If the condition is true, do the following:
synchronized(syncObject) {
    syncObject.notify();
}

syncObjectсам по собі може бути простим Object.

Є багато інших способів міжпотокового зв'язку, але який із них залежить, що саме ви робите.


1
Ласкаво просимо! Майте на увазі, що є й інші способи синхронізації, наприклад, семафори, блокування черг тощо ... все залежить від того, що ви хочете зробити. Об'єкти - чудові інструменти загальної мети для синхронізації потоків. Удачі вам у вашому додатку!
EboMike

12
Пробний улов слід обернути у цикл тестування реального базового стану, щоб уберегти від помилкового пробудження (див. Doco doco).
Лоуренс Дол

10
Варто зауважити, якщо спочатку викликається notifyAll, wait () буде чекати вічно, хоча умова була виконана ще до того, як почала чекати.
Пітер Лорі

3
Ця відповідь досить застаріла, оскільки вийшов java.concurent. Більш чистий і менш схильний до помилок спосіб очікування - використовувати CountDownLatch per Effective Java Ed 2
Алан Березін

1
@PeterLawrey Слід також зазначити (навіть більше ніж через вісім років після отримання цієї відповіді), що використання notfifyзамість notifyAllможе призвести до забавних ефектів, якщо інший потік також почне очікувати таким чином, оскільки notifyповідомлено лише один із потоків очікування (припустимо, що це випадковий).
Лотар,

53

Відповідь EboMike в і відповідь Тобі обидва на правильному шляху, але вони обидва містять фатальну помилку. Недолік називається втраченим сповіщенням .

Проблема полягає в тому, що якщо потік викликає foo.notify(), він взагалі нічого не зробить, якщо якийсь інший потік вже не перебуває у режимі foo.wait()виклику. Об'єкт, fooне пам'ятає, що про це було повідомлено.

Існує причина, чому вам не дозволено дзвонити foo.wait()або foo.notify()якщо потік не синхронізований на foo. Це тому, що єдиний спосіб уникнути втраченого сповіщення - захистити стан за допомогою мьютексу. Коли все зроблено правильно, це виглядає так:

Споживча нитка:

try {
    synchronized(foo) {
        while(! conditionIsTrue()) {
            foo.wait();
        }
        doSomethingThatRequiresConditionToBeTrue();
    }
} catch (InterruptedException e) {
    handleInterruption();
}

Виробнича нитка:

synchronized(foo) {
    doSomethingThatMakesConditionTrue();
    foo.notify();
}

Код, який змінює умову, і код, який перевіряє стан, синхронізуються на одному і тому ж об'єкті, і споживчий потік явно перевіряє стан перед тим, як він зачекає. Споживач не може пропустити повідомлення і назавжди застрягти у wait()дзвінку, коли умова вже відповідає дійсності.

Також зауважте, що wait()цикл знаходиться в циклі. Це тому, що, як правило, до того моменту, коли споживач знову отримує fooзамок і прокидається, якийсь інший потік міг знову зробити умовою помилковий. Навіть якщо у вашій програмі це неможливо, в деяких операційних системах можливе foo.wait()повернення, навіть якщо foo.notify()воно не викликане. Це називається фальшивим пробудженням , і це допускається, оскільки це полегшує впровадження в деяких операційних системах очікування / сповіщення.


1
Чи слід розміщувати try-catch всередині або поза циклом while? Який шлях рекомендується і чому?
Jaydev

1
@JaydevKalivarapu, якщо припустити, що ти запитуєш про це InterruptedException, так? Ви самі вирішуєте, що означає переривання, але в більшості випадків це, мабуть, означає "припиніть чекати" і зробіть щось інше (як, наприклад, вимкніть всю програму.) Отже, в більшості випадків вам цього буде потрібно щоб виглядати як у моєму прикладі вище, з обробником переривань поза циклом.
Соломон Повільний

6
@JaydevKalivarapu, PS: Коли я писав вищезазначену відповідь, я не знав, що цей шаблон має назву: Підручники з Oracle Java називають його захищеним блоком . Ви можете прочитати про це за адресою docs.oracle.com/javase/tutorial/essential/concurrency/…
Соломон Повільний

1
@JianGuo, foo.wait()кине, IllegalMonitorStateExceptionякщо fooне заблоковано. Це має нагадати вам, що не має сенсу wait()в коді, який не утримує блокування. Моя відповідь вище стосується причини, але якщо ви хочете детального пояснення, тоді вам слід прочитати підручник. docs.oracle.com/javase/tutorial/essential/concurrency/…
Соломон Повільний

1
@JianGuo, якщо ви це зробите, то що може статися - це; (1) Споживач перевіряє стан і виявляє його помилковим, (2) Споживач намагається увійти, synchronized(foo)але він заблокований, оскільки виробник вже синхронізований foo, (3) Виробник змушує умову стати істинною, дзвонить foo.notify()і потім відпускає замок, (4) споживач заходить у synchronized(foo)блок і телефонує foo.wait(). Тепер споживач застряг у очікуванні повідомлення, яке ніколи не надійде. Цю проблему іноді називають "втрачене сповіщення".
Соломон Повільний

25

Оскільки ніхто не публікував рішення з CountDownLatch. Що стосовно:

public class Lockeable {
    private final CountDownLatch countDownLatch = new CountDownLatch(1);

    public void doAfterEvent(){
        countDownLatch.await();
        doSomething();
    }

    public void reportDetonatingEvent(){
        countDownLatch.countDown();
    }
}

22

Подібно до відповіді EboMike, ви можете використовувати механізм, схожий на очікування / сповіщення / сповіщенняВсі, але пристосований для використання Lock.

Наприклад,

public void doSomething() throws InterruptedException {
    lock.lock();
    try {
        condition.await(); // releases lock and waits until doSomethingElse is called
    } finally {
        lock.unlock();
    }
}

public void doSomethingElse() {
    lock.lock();
    try {
        condition.signal();
    } finally {
        lock.unlock();
    }
}

Там, де ви будете чекати якоїсь умови, про яку повідомляє інший потік (в даному випадку виклик doSomethingElse), у цей момент перший потік продовжиться ...

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

Крім того, я не можу не помітити, як ви маєте справу з перерваним винятком у вашому прикладі. Ймовірно, ви не повинні споживати виняток таким чином, замість цього скиньте прапорець стану переривання за допомогою Thread.currentThread().interrupt.

Це тому, що якщо буде вилучено виняток, прапор стану переривання буде скинуто (він говорить: « Я більше не пам’ятаю, як мене перебивали, я не зможу повідомити нікому іншому, що я був, якщо вони запитують »), і інший процес може покладайтеся на це питання. Прикладом є те, що щось інше реалізувало політику переривання, засновану на цій ... Фу. Подальшим прикладом може бути те, що ваша політика переривання, а не така, що while(true)могла бути реалізована як while(!Thread.currentThread().isInterrupted()(що також зробить ваш код більш ... соціально уважним).

Отже, підсумовуючи, використання Conditionгрубо еквівалентно використанню wait / notify / notifyAll, коли ви хочете використовувати a Lock, реєстрація є злим, а ковтання InterruptedExceptionнеслухняним;)


2
Використовуючи Condition+ Lockце НЕ еквівалентно Objectметодів синхронізації + synchronized. Перші дозволяють сповіщення до очікування умови - з іншого боку, якщо ви телефонували Object.notify()раніше Object.wait(), потік назавжди заблокується. Крім того, його await()потрібно викликати в циклі, див. Документи.
TheOperator

@TheOperator Щодо "Колишні дозволити сповіщення до того, як очікується умова" - я переглянув Javadoc, Conditionале не зміг знайти текст, що підтверджує цю претензію. Чи можете ви пояснити своє твердження?
Наюкі

1
Приклад коду помилковий, await потрібно викликати у циклі. Див. Документ API щодо умови.
Натан Хьюз,

@TheOperator Object.wait()також повинен викликатися в циклі.
Сасса Н.Ф.

7

Ви можете використовувати семафор .

Поки умова не виконується, інший потік отримує семафор.
Ваш потік спробує отримати його за допомогою acquireUninterruptibly()
або tryAcquire(int permits, long timeout, TimeUnit unit)і буде заблокований.

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

Ви також можете спробувати використовувати a SynchronousQueueабо a CountDownLatch.


5

Беззамкове рішення (?)

У мене була та сама проблема, але я хотів рішення, яке не використовувало замки.

Проблема: у мене є щонайбільше один потік, який споживає з черги. Кілька потоків виробників постійно вставляють у чергу і повинні повідомити споживача, якщо він чекає. Черга не блокується, тому використання блокування для сповіщення спричиняє непотрібне блокування у потоках виробника. Кожен потік виробника повинен отримати замок, перш ніж він зможе повідомити споживача, який чекає. Я вважаю, що придумав рішення, що не містить замків, за допомогою LockSupportі AtomicReferenceFieldUpdater. Якщо в JDK існує перешкода без замків, я не міг її знайти. І те, CyclicBarrierі CoundDownLatchінше використовуйте замки від того, що я міг знайти.

Це мій дещо скорочений код. Щоб бути зрозумілим, цей код дозволить зачекати лише одному потоку. Він може бути модифікований, щоб дозволити численним офіціантам / споживачам, використовуючи певний тип атомної колекції для зберігання кількох власників ( ConcurrentMapможе працювати).

Я використав цей код, і він, здається, працює. Я його широко не тестував. Я пропоную вам прочитати документацію LockSupportперед використанням.

/* I release this code into the public domain.
 * http://unlicense.org/UNLICENSE
 */

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.LockSupport;

/**
 * A simple barrier for awaiting a signal.
 * Only one thread at a time may await the signal.
 */
public class SignalBarrier {
    /**
     * The Thread that is currently awaiting the signal.
     * !!! Don't call this directly !!!
     */
    @SuppressWarnings("unused")
    private volatile Thread _owner;

    /** Used to update the owner atomically */
    private static final AtomicReferenceFieldUpdater<SignalBarrier, Thread> ownerAccess =
        AtomicReferenceFieldUpdater.newUpdater(SignalBarrier.class, Thread.class, "_owner");

    /** Create a new SignalBarrier without an owner. */
    public SignalBarrier() {
        _owner = null;
    }

    /**
     * Signal the owner that the barrier is ready.
     * This has no effect if the SignalBarrer is unowned.
     */
    public void signal() {
        // Remove the current owner of this barrier.
        Thread t = ownerAccess.getAndSet(this, null);

        // If the owner wasn't null, unpark it.
        if (t != null) {
            LockSupport.unpark(t);
        }
    }

    /**
     * Claim the SignalBarrier and block until signaled.
     *
     * @throws IllegalStateException If the SignalBarrier already has an owner.
     * @throws InterruptedException If the thread is interrupted while waiting.
     */
    public void await() throws InterruptedException {
        // Get the thread that would like to await the signal.
        Thread t = Thread.currentThread();

        // If a thread is attempting to await, the current owner should be null.
        if (!ownerAccess.compareAndSet(this, null, t)) {
            throw new IllegalStateException("A second thread tried to acquire a signal barrier that is already owned.");
        }

        // The current thread has taken ownership of this barrier.
        // Park the current thread until the signal. Record this
        // signal barrier as the 'blocker'.
        LockSupport.park(this);
        // If a thread has called #signal() the owner should already be null.
        // However the documentation for LockSupport.unpark makes it clear that
        // threads can wake up for absolutely no reason. Do a compare and set
        // to make sure we don't wipe out a new owner, keeping in mind that only
        // thread should be awaiting at any given moment!
        ownerAccess.compareAndSet(this, t, null);

        // Check to see if we've been unparked because of a thread interrupt.
        if (t.isInterrupted())
            throw new InterruptedException();
    }

    /**
     * Claim the SignalBarrier and block until signaled or the timeout expires.
     *
     * @throws IllegalStateException If the SignalBarrier already has an owner.
     * @throws InterruptedException If the thread is interrupted while waiting.
     *
     * @param timeout The timeout duration in nanoseconds.
     * @return The timeout minus the number of nanoseconds that passed while waiting.
     */
    public long awaitNanos(long timeout) throws InterruptedException {
        if (timeout <= 0)
            return 0;
        // Get the thread that would like to await the signal.
        Thread t = Thread.currentThread();

        // If a thread is attempting to await, the current owner should be null.
        if (!ownerAccess.compareAndSet(this, null, t)) {
            throw new IllegalStateException("A second thread tried to acquire a signal barrier is already owned.");
        }

        // The current thread owns this barrier.
        // Park the current thread until the signal. Record this
        // signal barrier as the 'blocker'.
        // Time the park.
        long start = System.nanoTime();
        LockSupport.parkNanos(this, timeout);
        ownerAccess.compareAndSet(this, t, null);
        long stop = System.nanoTime();

        // Check to see if we've been unparked because of a thread interrupt.
        if (t.isInterrupted())
            throw new InterruptedException();

        // Return the number of nanoseconds left in the timeout after what we
        // just waited.
        return Math.max(timeout - stop + start, 0L);
    }
}

Щоб дати неясний приклад використання, я скористаюся прикладом Джеймса Великого:

SignalBarrier barrier = new SignalBarrier();

Споживча нитка (однина, а не множина! ):

try {
    while(!conditionIsTrue()) {
        barrier.await();
    }
    doSomethingThatRequiresConditionToBeTrue();
} catch (InterruptedException e) {
    handleInterruption();
}

Виробничі нитки:

doSomethingThatMakesConditionTrue();
barrier.signal();
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.