简体   繁体   English

这个java类线程安全吗?

[英]Is this java class thread safe?

This isn't homework for me , it's a task given to students from some university. 这不是我的功课这是给大学学生的一项任务。 I'm interested in the solution out of personal interest. 出于个人兴趣,我对解决方案感兴趣。

The task is to create a class (Calc) which holds an integer. 任务是创建一个包含整数的类(Calc)。 The two methods add and mul should add to or multiply this integer. 这两个方法add和mul应该添加或乘以这个整数。

Two threads are set-up simultaneously. 同时设置两个线程。 One thread should call c.add(3) ten times, the other one should call c.mul(3) ten times (on the same Calc-object of course). 一个线程应该调用c.add(3)十次,另一个应该调用c.mul(3)十次(当然在相同的Calc对象上)。

The Calc class should make sure that the operations are done alternatingly ( add, mul, add, mul, add, mul, ..). Calc类应确保操作交替完成(add,mul,add,mul,add,mul,..)。

I haven't worked with concurrency related problems a lot - even less with Java. 我没有经常处理与并发相关的问题 - 更不用说Java了。 I've come up with the following implementation for Calc: 我已经为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;  
        }
    }

}

I'd like to know if I'm on the right track here. 我想知道我是否走在正确的轨道上。 And there's surely a more elegant way to the while(b) part. 对于while(b)部分来说,肯定有一种更优雅的方式。 I'd like to hear your guys' thoughts. 我想听听你们这些人的想法。

PS: The methods' signature mustn't be changed. PS:不得更改方法的签名。 Apart from that I'm not restricted. 除此之外,我不受限制。

Try using the Lock interface: 尝试使用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();
        }
    }
}

The lock works like your synchronized blocks. 锁定就像你的同步块一样。 But the methods will wait at .await() if another thread holds the lock until .signal() is called. 但是如果另一个线程持有lock那么方法将在.await()处等待,直到.signal()

What you did is a busy loop: you're running a loop which only stops when a variable changes. 你所做的是一个繁忙的循环:你正在运行一个只在变量发生变化时停止的循环。 This is a bad technique because it makes the CPU very busy, instead of simple making the thread wait until the flag is changed. 这是一个糟糕的技术,因为它使CPU非常繁忙,而不是简单地使线程等待,直到更改标志。

I would use two semaphores : one for multiply , and one for add . 我会使用两个信号量 :一个用于multiply ,一个用于add add must acquire the addSemaphore before adding, and releases a permit to the multiplySemaphore when it's done, and vice-versa. 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();
    }
}

As others have said, the volatile in your solution is required. 正如其他人所说的volatile在您的解决方案是必需的。 Also, your solution spin-waits, which can waste quite a lot of CPU cycles. 此外,您的解决方案旋转等待,这可能会浪费相当多的CPU周期。 That said, I can't see any problems as far as correctness in concerned. 也就是说,就有关的正确性而言,我看不出任何问题。

I personally would implement this with a pair of semaphores: 我个人会用一对信号量来实现它:

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

Yes, volatile is needed, not because an assignment from a boolean to another is not atomic, but to prevent the caching of the variable such that its updated value is not visible to the other threads who are reading it. 是的,需要volatile ,不是因为从boolean到另一个boolean的赋值不是原子的,而是为了防止变量的缓存使得其更新的值对于正在读取它的其他线程是不可见的。 Also sum should be volatile if you care about the final result. 如果您关心最终结果, sum应该是volatile

Having said this, it would probably be more elegant to use wait and notify to create this interleaving effect. 话虽如此,使用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();
        }
    }
}

Then after you start both threads, call initiate to release the first thread. 然后在启动两个线程后,调用initiate以释放第一个线程。

volatile is needed otherwise the optimizer might optimize the loop to if(b)while(true){} 需要volatile,否则优化器可能会优化循环, if(b)while(true){}

but you can do this with wait and notify 但你可以通过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();
    }
}

however in this case (b checked inside the sync block) volatile is not needed 但是在这种情况下(b在同步块内检查)不需要volatile

Hmmm. 嗯。 There are a number of problems with your solution. 您的解决方案存在许多问题。 First, volatile isn't required for atomicity but for visibility. 首先,原子性不需要挥发性,而是可见性。 I won't go into this here, but you can read more about the Java memory model . 我不会在这里讨论,但您可以阅读有关Java内存模型的更多信息。 (And yes, boolean is atomic, but it's irrelevant here). (是的,布尔是原子的,但这里无关紧要)。 Besides, if you access variables only inside synchronized blocks then they don't have to be volatile. 此外,如果仅在同步块访问变量,则它们不必是易失性的。

Now, I assume that it's by accident, but your b variable is not accessed only inside synchronized blocks, and it happens to be volatile, so actually your solution would work, but it's neither idiomatic nor recommended, because you're waiting for b to change inside a busy loop. 现在,我认为这是偶然的,但是你的b变量只是在同步块中才被访问,而且它恰好是易变的,所以实际上你的解决方案会起作用,但它既不是惯用的也不是推荐的,因为你在等待b到在繁忙的循环中改变。 You're burning CPU cycles for nothing (this is what we call a spin-lock, and it may be useful sometimes). 你没有任何东西燃烧CPU周期(这就是我们所谓的自旋锁,有时它可能很有用)。

An idiomatic solution would look like this: 惯用解决方案如下所示:

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

The program is fully thread safe: 该程序是完全线程安全的:

  1. The boolean flag is set to volatile, so the JVM knows not to cache values and to keep write-access to one thread at a time. 布尔标志设置为volatile,因此JVM知道不缓存值并且一次保持对一个线程的写访问。

  2. The two critical sections locks on the current object, which means only one thread will have access at a time. 两个关键部分锁定当前对象,这意味着一次只有一个线程可以访问。 Note that if a thread is inside the synchronized block, no thread can be in any other critical sections. 请注意,如果一个线程在同步块内,则任何其他关键部分都不能有线程。

The above will apply to every instance of the class. 以上将适用于该类的每个实例。 For example if two instances are created, threads will be able to enter multiple critical sections at a time, but will be limited to one thread per instances, per critical section. 例如,如果创建了两个实例,则线程将能够一次输入多个关键部分,但每个关键部分将限制为每个实例一个线程。 Does that make sense? 那有意义吗?

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

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