【问题标题】:Is this java class thread safe?这个java类线程安全吗?
【发布时间】:2012-02-17 01:31:39
【问题描述】:

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

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

【问题讨论】:

  • 您可以改用AtomicBoolean & AtomicInteger
  • @AviramSegal - 详细说明:这是 1 个线程连续调用 mul() 十次。不是十个线程每个调用 mul()。
  • 线程安全与否,任何以这种方式使用布尔值作为学生作业的人都应该被淘汰。字段名称b。太好了,太好了。
  • @owlstead 嗯,我永远不会命名这样的布尔值。但是因为示例代码太短了,我觉得它并不是真正需要的。 Ab(使用)布尔值作为一种开关也很丑陋,但是我真的只关心线程安全部分。
  • 虽然我确实理解简洁的要求,但我确实看到很多新的开发人员似乎接管了这种简洁,我不得不忍受他们产生的代码。但请原谅我跑题了。关键是即使它是线程安全的,也不是要走的路。

标签: java multithreading concurrency synchronized


【解决方案1】:

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

【讨论】:

  • 如果您修改示例以包含两个条件(以处理操作的交替),我会赞成。
  • 嗯,我不确定使用相同的信号变量是否是线程安全的。可能存在调用 add 的线程会发出 allowAccess 信号然后重新获取锁本身并发现它发出信号的情况。
  • 你的意思是因为加法必须先进行?我忘了那一点。
  • @Perception 编辑为使用两个条件。
  • 有人试过执行这段代码吗?这不会导致死锁吗?当线程 T1 获得锁定并 等待 调用 mul (multiplication.await())。当 mul 被 T2 调用时,它会获得锁并 等待 调用 add (addition.await()) 导致死锁
【解决方案2】:

您所做的是一个繁忙的循环:您正在运行一个仅在变量更改时才停止的循环。这是一种糟糕的技术,因为它会使 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();
    }
}

【讨论】:

  • 我认为 ReentrantLock 版本更容易理解。就是说,这个似乎对初始状态更清楚,并且可能更具适应性,所以 +1 你去。
【解决方案3】:

正如其他人所说,您的解决方案中的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();
}

【讨论】:

  • 我们的想法完全一样!
  • @JBNizet:是的,但是你更快地找到了这个解决方案(在决定信号量是最好的解决方案之前,我花了很多时间修补其他原语。)
  • 所以?发展不是速度竞赛。如果是这样,我会浪费很多时间。不幸的是,在更多方面,stackoverflow 是。
【解决方案4】:

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

【讨论】:

  • 我会使用 notifyAll,以防万一需求发生变化并且您可能有多个加法和/或乘法线程(在这种情况下您会遇到死锁)。
  • 您是否有任何参考资料来支持关于优化掉非易失性事物的声明?我理解它更多地是关于可见性......或者这是同一问题的一部分?任何有助于更好理解的链接将不胜感激:)
  • @Toby 检查this blog post
【解决方案5】:

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

【讨论】:

  • 当然,Java 规范实际上并不能保证任何进展。 /我相信synchronized在这里是不必要的。/但是忙吗?
  • +1 解决问题。我以前读过它,但完全忘记了。
  • @s3rius:我使用等待和通知对一些代码进行了编辑。对我来说,这似乎更清楚。
  • sum 不必是易失性的,因为它受到同步块的保护(只要在这样的块内读取)。
  • +1 为您的易变解释。我不确定我是否同意以前的 cmets 说如果没有它,事情会得到优化。你对此有何评论?我理解它更多的是关于可见性而不是防止优化......
【解决方案6】:

嗯。您的解决方案存在许多问题。首先, volatile 不是原子性所必需的,而是可见性所必需的。我不会在这里讨论这个,但你可以阅读更多关于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();
    }
}

【讨论】:

  • 问题 - while(!b) wait(); 之间会有区别吗?如果(!b)等待(); ?
  • @s3rius 是的。您必须始终在 while 循环中测试监视器条件(您 wait() 等待的条件)。有两个原因。首先,您不知道线程被唤醒的原因(使用 notify() 或 notifyAll())。可能是因为您正在测试的条件发生了变化,也可能是其他原因。其次,在某些硬件/操作系统架构上,可能存在虚假的线程唤醒,即根本没有充分理由的唤醒。我相信,这在大多数架构中不会发生,但仍然存在这样的习惯用法:您必须始终测试您在 while 循环中等待的条件。
  • +1 用于描述虚假唤醒和循环条件检查的需要:)
【解决方案7】:

程序是完全线程安全的:

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

  2. 两个临界区锁定当前对象,这意味着一次只有一个线程可以访问。请注意,如果一个线程在同步块内,则没有线程可以在任何其他临界区。

以上将适用于类的每个实例。例如,如果创建了两个实例,线程将能够一次进入多个临界区,但每个实例、每个临界区将被限制为一个线程。那有意义吗?

【讨论】:

  • 但可以改进。问题是什么。
猜你喜欢
  • 2011-08-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多