Совместное использование значения локальной переменной между барьерными потоками в java

#java #concurrency #barrier #shared-variable

Вопрос:

Я работал над реализацией пользовательского циклического барьера, который добавляет значения, переданные в метод await, и возвращает сумму всем потокам при вызове after notify .

Код:

 public class Barrier {

    private final int parties;
    private int partiesArrived = 0;

    private volatile int sum = 0;
    private volatile int oldSum = 0;

    public Barrier(int parties) {
        if (parties < 1) throw new IllegalArgumentException("Number of parties has to be 1 or higher.");
        this.parties = parties;
    }

    public int getParties() { return parties; }

    public synchronized int waitBarrier(int value) throws InterruptedException {
        partiesArrived  = 1;
        sum  = value;
        if (partiesArrived != parties) {
            wait();
        }
        else {
            oldSum = sum;
            sum = 0;
            partiesArrived = 0;
            notifyAll();
        }
        return oldSum;
    }

    public int getNumberWaiting() { return partiesArrived; }
}

 

Это работает, но я слышал, что есть способ изменить значения sum и oldSum (или, по крайней мере oldSum ) в локальные переменные waitBarrier метода. Однако, ломая голову над этим, я не вижу способа.

Возможно ли это и, если да, то как?

Комментарии:

1. oldSum , конечно. Но как может sum быть локальная переменная? Потоки не могут совместно использовать локальные переменные. Как потоки могут вычислять сумму, если они не делятся ею друг с другом?

2. @SolomonSlow, можете ли вы подробнее рассказать о том, как oldSum можно преобразовать в локальную переменную?

3. Хм, … удалите объявление на уровне класса oldSum и объявите int oldSum внутри waitBarrier(...) функции.

4. @SolomonSlow не сработает. Суммы не будут одинаковыми

5. То же самое, что и что? Есть только одна сумма. Вы используете его только oldSum как временное хранилище для его хранения, чтобы можно было установить sum = 0 , прежде чем возвращать его предыдущее значение.

Ответ №1:

Однако, ломая голову над этим, я не вижу способа.

Именно так.

Возможно ли это и, если да, то как?

это невозможно.

Для каких- то доказательств:

Попробуйте пометить локальную переменную как volatile . Это не сработает: компилятор этого не допускает. Почему этого не происходит? Потому что volatile — это обязательно no-op: локальные переменные просто не могут быть переданы другому потоку.

Можно подумать, что это «совместное использование» локального:

 void test() {
   int aLocalVar = 10;
   Thread t = new Thread(() -> {
     System.out.println("Wow, we're sharing it! "   aLocalVar);
   });
   t.start();
}
 

Но это какой-то синтаксический сахар, сбивающий вас с толку: на самом деле (и вы можете подтвердить это с javap -c -v помощью показать байт-код, который javac создает этот код), копия локального var передается блоку здесь. Затем это объясняет, почему в java вышеуказанное не удается скомпилировать, если переменная, которую вы пытаетесь разделить, не помечена либо [A] final , либо [B] могла быть помечена таким образом без ошибок (это называется «переменная фактически окончательная»).). Если бы java позволяла вам получать доступ к не (эффективным) финалам, подобным этому, и если бы java использовала доступный механизм копирования, это было бы невероятно запутанным.

Конечно, в java все непримитивы являются ссылками. Указатели, на языке некоторых других языков. Таким образом, вы можете «поделиться» (не совсем, это будет копия) локальной переменной и, тем не менее, получить то, что вы хотите (разделить состояние между 2 потоками), потому что, пока вы получаете копию переменной, переменная является просто указателем. Это примерно так: если у меня есть лист бумаги, и он мой, но я могу бросить его в ксерокс и тоже дать вам копию, мы, по-видимому, не можем разделить состояние. Что бы я ни нацарапал на своей бумаге, оно волшебным образом не появится на вашей; это не бумага вуду. Но если на моей бумаге есть адрес дома, и я копирую его и передаю вам копию, создается ощущение, что мы делимся этим: если вы подойдете к дому и, я не знаю, бросите кирпич в окно, а я подойду позже, я его увижу.

Многие объекты в java являются неизменяемыми (непроницаемыми для кирпичей), а примитивы не являются ссылками. Одним из решений является использование AtomicX семейства, которые представляют собой просто упрощенные оболочки вокруг примитива или ссылки, что делает их изменяемыми:

 AtomicInteger v = new AtomicInteger();
Thread t = new Thread(() -> {v.set(10);});
t.start();
t.yield();
System.out.println(t.get());
// prints 10
 

Но здесь не произошло фактического совместного использования локальной переменной. Поток получил копию ссылки на один экземпляр AtomicInteger, который находится в куче, и оба потока закончили тем, что «перешли к дому», здесь.

Ответ №2:

Вы можете вернуться sum и попросить первую сторону очистить его:

 public synchronized int waitBarrier(int value) throws InterruptedException {
    if (partiesArrived == 0) {
        sum = 0;
    }

    partiesArrived  ;
    sum  = value;

    if (partiesArrived == parties) {
        notifyAll();
    } else {
        while (partiesArrived < parties) {
            wait();
        }
    }

    return sum;
}
 

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

Комментарии:

1. То, что вы сказали, верно, но это не отвечает на вопрос OP, который был: «Как это могло бы работать, если sum бы это была локальная переменная?»

2. @SolomonSlow (или, по крайней мере oldSum ,)