Java Concurrency JDK 1.6: занятое ожидание работает лучше, чем сигнализация?Эффективная Java # 51 - PullRequest
6 голосов
/ 22 июля 2010

«Эффективная Java» Джошуа Блоха, пункт 51, не касается зависимости от планировщика потоков, а также не держит потоки без необходимости в состоянии выполнения. Цитируемый текст:

Основная техника для уменьшения количества работающих потоков - это иметь каждый поток сделать небольшой объем работы, а затем ждать какого-то условия с помощью Object.wait или для некоторых время истекло, используя Thread.sleep. Потоки не должны быть заняты - ждать, повторно проверяя данные структура ждет чего-то, чтобы случиться. Помимо того, что делает программу уязвимой для капризы планировщика, занятое ожидание может значительно увеличить нагрузку на процессор, сокращение объема полезной работы, которую другие процессы могут выполнять на той же машине.

А затем показывает микробенчм занятого ожидания и правильного использования сигналов. В книге ожидание «занято» выполняет 17 рейсов в секунду, а версия «ожидание / уведомление» - 23 000 рейсов в секунду.

Однако, когда я попробовал тот же самый тест на JDK 1.6, я вижу наоборот: занятое ожидание делает 760K циклов / секунду, тогда как версия ожидания / уведомления делает 53,3K циклов / с - то есть ожидание / уведомление был в ~ 1400 раз быстрее, но оказывается в ~ 13 раз медленнее?

Я понимаю, что ожидание занятости не является хорошим, а сигнализация все еще лучше - загрузка процессора составляет ~ 50% в версии ожидания занятости, тогда как в версии ожидания / уведомления она остается на уровне ~ 30% - но есть ли что-то, что объясняет цифры?

Если это поможет, я использую JDK1.6 (32-разрядную версию) на Win 7 x64 (core i5).

ОБНОВЛЕНИЕ : источник ниже. Чтобы запустить занятую рабочую среду, измените базовый класс PingPongQueue на BusyWorkQueue import java.util.LinkedList; import java.util.List;

abstract class SignalWorkQueue { 
    private final List queue = new LinkedList(); 
    private boolean stopped = false; 

    protected SignalWorkQueue() { new WorkerThread().start(); } 

    public final void enqueue(Object workItem) { 
        synchronized (queue) { 
            queue.add(workItem); 
            queue.notify(); 
        } 
    } 

    public final void stop()  { 
        synchronized (queue) { 
            stopped = true; 
            queue.notify(); 
        } 
    } 
    protected abstract void processItem(Object workItem) 
        throws InterruptedException; 
    private class WorkerThread extends Thread { 
        public void run() { 
            while (true) {  // Main loop 
                Object workItem = null; 
                synchronized (queue) { 
                    try { 
                        while (queue.isEmpty() && !stopped) 
                            queue.wait(); 
                    } catch (InterruptedException e) { 
                        return; 
                    } 
                    if (stopped) 
                        return; 
                    workItem = queue.remove(0); 
                } 
                try { 
                    processItem(workItem); // No lock held 
                } catch (InterruptedException e) { 
                    return; 
                } 
            } 
        } 
    } 
}

// HORRIBLE PROGRAM - uses busy-wait instead of Object.wait! 
abstract class BusyWorkQueue {
    private final List queue = new LinkedList();
    private boolean stopped = false;

    protected BusyWorkQueue() {
        new WorkerThread().start();
    }

    public final void enqueue(Object workItem) {
        synchronized (queue) {
            queue.add(workItem);
        }
    }

    public final void stop() {
        synchronized (queue) {
            stopped = true;
        }
    }

    protected abstract void processItem(Object workItem)
            throws InterruptedException;

    private class WorkerThread extends Thread {
        public void run() {
            final Object QUEUE_IS_EMPTY = new Object();
            while (true) { // Main loop
                Object workItem = QUEUE_IS_EMPTY;
                synchronized (queue) {
                    if (stopped)
                        return;
                    if (!queue.isEmpty())
                        workItem = queue.remove(0);
                }

                if (workItem != QUEUE_IS_EMPTY) {
                    try {
                        processItem(workItem);
                    } catch (InterruptedException e) {
                        return;
                    }
                }
            }
        }
    }
}

class PingPongQueue extends SignalWorkQueue {
    volatile int count = 0;

    protected void processItem(final Object sender) {
        count++;
        SignalWorkQueue recipient = (SignalWorkQueue) sender;
        recipient.enqueue(this);
    }
}

public class WaitQueuePerf {
    public static void main(String[] args) {
        PingPongQueue q1 = new PingPongQueue();
        PingPongQueue q2 = new PingPongQueue();
        q1.enqueue(q2); // Kick-start the system

        // Give the system 10 seconds to warm up
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
        }

        // Measure the number of round trips in 10 seconds
        int count = q1.count;
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
        }
        System.out.println(q1.count - count);

        q1.stop();
        q2.stop();
    }
}

Ответы [ 4 ]

6 голосов
/ 22 июля 2010

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

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

Так что это зависит. Если вы заняты ожиданием ввода пользователя, это определенно неправильно; в то время как ожидание занятости в структурах данных без блокировки, таких как AtomicInteger, определенно хорошо.

3 голосов
/ 22 июля 2010

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

Попробуйте запустить 1000 потоков ожидания занятости против 1000 ожидания /уведомлять потоки и проверять вашу общую пропускную способность.

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

1 голос
/ 22 июля 2010

Это зависит от количества потоков и степени конфликтов: занятые ожидания плохие, если они случаются часто и / или потребляют много циклов ЦП.

Но атомные целые числа (AtomicInteger, AtomicIntegerArray ...)лучше, чем синхронизация Integer или int [], даже поток также выполняет занятое ожидание.

Используйте пакет java.util.concurrent, а в вашем случае ConcurrentLinkedQueueas как можно чаще

0 голосов
/ 28 августа 2013

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

На самом деле, если вы прогуляетесь по источникам пакетов java.util.concurrentВы найдете много мест, содержащих «хитрый», на первый взгляд хрупкий код.Я считаю SynchronousQueue хорошим примером (вы можете взглянуть на источник в дистрибутиве JDK или здесь , и OpenJDK, и Oracle, похоже, используют одну и ту же реализацию).Оживленное ожидание используется в качестве оптимизации - после определенного количества «спинов» поток переходит в надлежащий «спящий режим».Помимо этого, у него есть и другие тонкости - волатильное комбинирование, порог вращения, зависящий от количества процессоров и т. Д. Он действительно ... освещает то, что показывает, что требуется для реализации эффективного низкоуровневого параллелизма.Более того, код сам по себе действительно чистый, хорошо документированный и в целом качественный.

...