Является ли этот класс классов Java безопасным?

Это не домашнее задание для меня, это задание для студентов из какого-то университета. Меня интересует решение из личных интересов.

Задача состоит в создании класса (Calc), который содержит целое число. Два метода add и mul должны добавить или умножить это целое число.

Одновременно устанавливаются два потока. Один поток должен называть c.add(3) десять раз, другой должен вызвать c.mul(3) десять раз (на том же самом Calc-объекте, конечно).

Класс Calc должен следить за тем, чтобы операции выполнялись поочередно (add, mul, add, mul, add, mul,..).

Я не работал с concurrency связанными проблемами много - даже с Java. Я придумал следующую реализацию для Calc:

class Calc{

    private int sum = 0;
    //Is volatile actually needed? Or is bool atomic by default? Or it read operation, at least.
    private volatile bool b = true;

    public void add(int i){
        while(!b){}
        synchronized(this){
                sum += i;
            b = true;   
        }
    }

    public void mul(int i){
        while(b){}
        synchronized(this){
            sum *= i;
            b = false;  
        }
    }

}

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

PS: Подпись методов не должна изменяться. Кроме того, я не ограничен.

Ответ 1

требуется volatile, иначе оптимизатор может оптимизировать цикл до if(b)while(true){}

но вы можете сделать это с помощью wait и notify

public void add(int i){

    synchronized(this){
        while(!b){try{wait();}catch(InterruptedException e){}}//swallowing is not recommended log or reset the flag
            sum += i;
        b = true;   
        notify();
    }
}

public void mul(int i){
    synchronized(this){
        while(b){try{wait();}catch(InterruptedException e){}}
        sum *= i;
        b = false;  
        notify();
    }
}

однако в этом случае (b проверено внутри блока синхронизации) летучесть не требуется

Ответ 2

Попробуйте использовать интерфейс Lock:

class Calc {

    private int sum = 0;
    final Lock lock = new ReentrantLock();
    final Condition addition  = lock.newCondition(); 
    final Condition multiplication  = lock.newCondition(); 

    public void add(int i){

        lock.lock();
        try {
            if(sum != 0) {
                multiplication.await();
            }
            sum += i;
            addition.signal(); 

        } 
        finally {
           lock.unlock();
        }
    }

    public void mul(int i){
        lock.lock();
        try {
            addition.await();
            sum *= i;
            multiplication.signal(); 

        } finally {
           lock.unlock();
        }
    }
}

Блокировка работает как ваши синхронизированные блоки. Но методы будут ждать в .await(), если другой поток содержит lock, пока не будет вызван .signal().

Ответ 3

То, что вы сделали, это цикл занятости: вы запускаете цикл, который останавливается только при изменении переменной. Это плохой метод, потому что он делает CPU очень занятым, а не просто делает поток до тех пор, пока не будет изменен флаг.

Я бы использовал два семафора: один для multiply и один для add. add должен получить addSemaphore перед добавлением и освободить разрешение для multiplySemaphore, когда это будет сделано, и наоборот.

private Semaphore addSemaphore = new Semaphore(1);
private Semaphore multiplySemaphore = new Semaphore(0);

public void add(int i) {
    try {
        addSemaphore.acquire();
        sum += i;
        multiplySemaphore.release();
    }
    catch (InterrupedException e) {
        Thread.currentThread().interrupt();
    }
}

public void mul(int i) {
    try {
        multiplySemaphore.acquire();
        sum *= i;
        addSemaphore.release();
    }
    catch (InterrupedException e) {
        Thread.currentThread().interrupt();
    }
}

Ответ 4

Как говорили другие, требуется volatile в вашем решении. Кроме того, ваше решение затягивается, что может привести к большому количеству циклов процессора. Тем не менее, я не вижу никаких проблем с точки зрения правильности в отношении.

Я лично реализовал бы это с помощью пары семафоров:

private final Semaphore semAdd = new Semaphore(1);
private final Semaphore semMul = new Semaphore(0);
private int sum = 0;

public void add(int i) throws InterruptedException {
    semAdd.acquire();
    sum += i;
    semMul.release();
}

public void mul(int i) throws InterruptedException {
    semMul.acquire();
    sum *= i;
    semAdd.release();
}

Ответ 5

Да, volatile требуется, а не потому, что назначение из boolean в другое не является атомарным, но для предотвращения кэширования переменной, чтобы ее обновленное значение не было видно другим тем, кто его читает, Кроме того, sum должен быть volatile, если вам нужен окончательный результат.

Сказав это, было бы более элегантно использовать wait и notify для создания этого эффекта перемежения.

class Calc{

    private int sum = 0;
    private Object event1 = new Object();
    private Object event2 = new Object();

    public void initiate() {
        synchronized(event1){
           event1.notify();
        }
    }

    public void add(int i){
        synchronized(event1) {
           event1.wait();
        }
        sum += i;
        synchronized(event2){
           event2.notify();
        }
    }

    public void mul(int i){
        synchronized(event2) {
           event2.wait();
        }
        sum *= i;
        synchronized(event1){
           event1.notify();
        }
    }
}

Затем после запуска обоих потоков вызовите initiate, чтобы освободить первый поток.

Ответ 6

Хммм. В вашем решении есть ряд проблем. Во-первых, летучесть не требуется для атомарности, но для видимости. Я не буду вдаваться в это здесь, но вы можете больше узнать о модели памяти Java. (И да, boolean является атомарным, но это не имеет значения здесь). Кроме того, если вы обращаетесь к переменным только внутри синхронизированных блоков, тогда они не должны быть неустойчивыми.

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

Идиоматическое решение будет выглядеть так:

class Code {
    private int sum = 0;
    private boolean nextAdd = true;

    public synchronized void add(int i) throws InterruptedException {
        while(!nextAdd )
            wait();
        sum += i;
        nextAdd = false;
        notify();
    }

    public synchronized void mul(int i) throws InterruptedException {
        while(nextAdd)
            wait();
        sum *= i;
        nextAdd = true;
        notify();
    }
}

Ответ 7

Программа полностью потокобезопасна:

  • Логический флаг имеет значение volatile, поэтому JVM знает, что он не кэширует значения и не поддерживает запись в один поток за раз.

  • Две критические секции блокируются для текущего объекта, что означает, что только один поток будет иметь доступ одновременно. Обратите внимание, что если поток находится внутри синхронизированного блока, ни один поток не может быть в каких-либо других критических разделах.

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