繁体   English   中英

这个java类线程安全吗?

[英]Is this java class thread safe?

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

任务是创建一个包含整数的类(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:不得更改方法的签名。 除此之外,我不受限制。

尝试使用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();
        }
    }
}

锁定就像你的同步块一样。 但是如果另一个线程持有lock那么方法将在.await()处等待,直到.signal()

你所做的是一个繁忙的循环:你正在运行一个只在变量发生变化时停止的循环。 这是一个糟糕的技术,因为它使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();
    }
}

正如其他人所说的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();
}

是的,需要volatile ,不是因为从boolean到另一个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以释放第一个线程。

需要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

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

现在,我认为这是偶然的,但是你的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();
    }
}

该程序是完全线程安全的:

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

  2. 两个关键部分锁定当前对象,这意味着一次只有一个线程可以访问。 请注意,如果一个线程在同步块内,则任何其他关键部分都不能有线程。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM