这个java类线程安全吗?

时间:2012-01-23 17:39:13

标签: java multithreading concurrency synchronized

这不是我的作业 ,这是给大学学生的一项任务。出于个人兴趣,我对解决方案感兴趣。

任务是创建一个包含整数的类(Calc)。这两个方法add和mul应该添加或乘以这个整数。

同时设置两个线程。一个线程应该调用c.add(3)十次,另一个应该调用c.mul(3)十次(当然在同一个Calc对象上)。

Calc类应该确保操作是交替完成的(add,mul,add,mul,add,mul,..)。

我没有经常处理并发相关问题 - 更不用说Java了。我已经为Calc提出了以下实现:

class Calc{

    private int sum = 0;
    //Is volatile actually needed? Or is bool atomic by default? Or it's 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;  
        }
    }

}

我想知道我是否走在正确的轨道上。对于while(b)部分来说,肯定有一种更优雅的方式。 我想听听你们这些人的想法。

PS:不得更改方法的签名。除此之外,我不受限制。

7 个答案:

答案 0 :(得分:11)

尝试使用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()为止。

答案 1 :(得分:10)

你所做的是一个繁忙的循环:你正在运行一个只在变量发生变化时停止的循环。这是一个糟糕的技术,因为它使CPU非常繁忙,而不是简单地让线程等到标志发生变化。

我会使用两个semaphores:一个用于multiply,一个用于addadd必须在添加之前获取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();
    }
}

答案 2 :(得分:5)

正如其他人所说,解决方案中的volatile是必需的。此外,您的解决方案旋转等待,这可能会浪费相当多的CPU周期。也就是说,就所涉及的正确性而言,我看不出任何问题。

我个人会用一对信号量来实现它:

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();
}

答案 3 :(得分:3)

是的,需要volatile,不是因为从boolean到另一个的赋值不是原子的,而是为了防止变量的缓存使得其更新的值对其他线程不可见谁在读它。如果您关心最终结果,sum也应为volatile

说到这一点,使用waitnotify来创建这种交错效果可能会更优雅。

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以释放第一个线程。

答案 4 :(得分:3)

需要volatile,否则优化器可能会将循环优化为if(b)while(true){}

但您可以使用waitnotify

执行此操作
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在同步块内部检查)不需要volatile;

答案 5 :(得分:3)

嗯。您的解决方案存在许多问题。首先,原子性不需要挥发性,而是可见性。我不会在此处讨论,但您可以阅读有关Java memory model的更多信息。 (是的,布尔是原子的,但这里无关紧要)。此外,如果仅在同步块内访问变量,则它们不必是易失性的。

现在,我认为这是偶然的,但是你的b变量只是在同步块中才被访问,而且它碰巧是不稳定的,所以实际上你的解决方案是可行的,但它既不是惯用的,也不是推荐的,因为你在等待让b在繁忙的循环中改变。你没有任何东西燃烧CPU周期(这就是我们所谓的自旋锁,有时它可能很有用)。

惯用解决方案如下所示:

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();
    }
}

答案 6 :(得分:0)

程序是完全线程安全的:

  1. 布尔标志设置为volatile,因此JVM知道不会缓存值并且一次保持对一个线程的写访问权。

  2. 两个关键部分锁定当前对象,这意味着一次只能有一个线程访问。请注意,如果某个线程位于synchronized块内,则任何其他关键部分都不能包含任何线程。

  3. 以上内容适用于班级的每个实例。例如,如果创建了两个实例,则线程将能够一次输入多个关键部分,但每个关键部分将限制为每个实例一个线程。那有意义吗?