Почему блок кода Java быстрее при использовании синхронизированного ключевого слова, а не без него? - PullRequest
0 голосов
/ 27 октября 2019

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

Подход 1 :

class BoundedBlockingQueue {
    int capacity;
    Queue<Integer> queue;

    public BoundedBlockingQueue(int capacity) {
        this.capacity = capacity;
        this.queue = new LinkedList();
    }

    public void enqueue(int element) throws InterruptedException {
        while(queue.size() == capacity);
        queue.add(element);
    }

    public int dequeue() throws InterruptedException {
        while(queue.size() == 0);
        return queue.remove();
    }

    public int size() {
        return queue.size();
    }
}

Здесь, во время постановки в очередь, процесс будет продолжать цикл цикла while (обратите внимание на немедленный ; сразу после условия while)и продолжайте работу только тогда, когда queue.size () станет меньше емкости. Аналогичная логика существует для удаления из очереди, когда queue.size () равняется 0.

Второй способ создать то же самое - использовать ключевое слово synchronized, как показано ниже:

Подход 2 :

class BoundedBlockingQueue {
    int capacity;
    Queue<Integer> queue;

    public BoundedBlockingQueue(int capacity) {
        this.capacity = capacity;
        this.queue = new LinkedList();
    }

    public void enqueue(int element) throws InterruptedException {
        synchronized(queue){
            while(queue.size() == capacity) queue.wait();
            queue.add(element);
            queue.notifyAll();
        }
    }

    public int dequeue() throws InterruptedException {
        synchronized(queue){
            while(queue.size() == 0) queue.wait();
            int val = queue.remove();
            queue.notifyAll();
            return val;
        }
    }

    public int size() {
        return queue.size();
    }
}

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

Наблюдение состоит в том, что подход 1 занимает значительно больше времени выполнения, чем подход 2. Почему это так? Разве основная логика обоих подходов не одинакова? Почему для подхода 1 требуется больше времени выполнения по сравнению с подходом 2?

Любая помощь будет высоко оценена.

Ответы [ 2 ]

1 голос
/ 27 октября 2019

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

  • Представьте себе потоки № 1 и 2, работающие в строке while(queue.size() == 0);.
  • Поток № 3 вызывает вызов потока enqueue()
  • Потоки 1 и 2 (одновременно) отмечают, что условие цикла while неверно, поскольку размер очереди теперь равен 1, и оба одинаковыраз попробуй позвонить queue.remove(). Здесь один из потоков выдаст исключение (даже если LinkedList был потокобезопасным). Я предполагаю, что это не тот контракт, которого вы пытаетесь достичь.

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

1 голос
/ 27 октября 2019

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

Возможно capacity все еще инициализируется значением по умолчанию 0, когда enqueue называется, и поэтому ваш код истекает, кто знает.

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

Посмотрите на Lock и Condition если вы хотите реализовать поточно-ориентированные очереди-подобные структуры.

Третий момент заключается в том, что вместо использования пустой конструкции while следует использовать Thread.onSpinWait.

Я не уверен, что ваш второй подход также верен, потому что queue не final и может быть null при вызове synchronized(queue). Но я не уверен, что это действительно может произойти здесь. Но нет никакой причины для queue быть не final в любом случае.

...