【问题标题】:Is Java volatile keyword necessary if variable gets accessed by only one Thread, but passed through lambda of other Thread?如果变量仅由一个线程访问,但通过其他线程的 lambda 传递,Java volatile 关键字是否必要?
【发布时间】:2022-01-31 19:14:47
【问题描述】:

该问题仅涉及当前提供的示例(不是一般性的):

在这里省略mutableVariable 的“volatile”关键字是否安全,或者在线程安全方面绝对有必要添加它?

mutableVariable 只能在 Main-Thread 中访问,但由于 lambda 表达式,我不知道它是否通过 Thread-2 “传递”,因此可以缓存在那里,或者 Thread-2 只看到缓存值?

感谢您的回答和最诚挚的问候。

package test.java;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.junit.Test;

public class A {

    private final BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<Runnable>();
    private int mutableVariable = 1; // is "volatile"-keyword absolutely necessary here, or can it be omitted?!


    @Test
    public void test() throws InterruptedException {

        // executed in Thread-Main
        final Thread thread = new Thread(() -> {
            // executed in Thread-2
            this.blockingQueue.add(() -> {
                // executed in Thread-Main
                this.mutableVariable++;
                System.out.println(this.mutableVariable); // should always be 3
            });
        });

        // executed in Thread-Main
        this.mutableVariable = 2;
        thread.start();
        final Runnable r = this.blockingQueue.take();
        r.run();
    }

}

【问题讨论】:

  • Is it safe to omit the the "volatile"-keyword for mutableVariable here 在这个非常具体的情况下,是的。
  • TLDR:新线程持有对 lambda 的引用这一事实无关紧要:唯一重要的是哪个线程执行 lambda 主体。只有主线程会这样做。

标签: java multithreading lambda thread-safety volatile


【解决方案1】:

(警告:我不是并发专家。)

鉴于您的具体情况,是的,这在我看来是线程安全的,不需要volatile。但不推荐。

是的,线程安全

在跨线程使用之前建立的对象

我说线程安全是因为线程在一个实例方法中,所以我们知道A 对象会在该方法被调用之前被实例化。由于 mutableVariable 在其声明中被初始化,我们知道对象的存在将在线程相关方法被调用之前开始。由于变量是在线程之前明确建立的,而且你只能在线程方法内改变它的值,所以不需要volatile

只有一个后台线程

我说线程安全是因为你只启动了一个线程。

如果涉及多个线程,此代码:

this.mutableVariable++;
// Something other thread might be incrementing this variable at this point, between these statements. 
System.out.println(this.mutableVariable); 

... 会间歇性地失败,并打印一个由其他线程设置的值。

不,不推荐

我说不推荐,因为这里需要的条件太多了,都是隐含的,所以这个代码在实践中会很脆弱。以后的程序员可以很容易地打破这些条件之一。例如,作为A 上的普通成员字段,任何编写其他方法的人都会假定他们有权修改该值。这样的修改是不安全的。

AtomicInteger

相反,我建议在可行的情况下写self-documenting code。特别是,使用Atomic… 类会使线程安全问题非常明显。

在你的情况下,我会使用AtomicInteger。所以这个:

private int mutableVariable = 1 ; 

…变成这样:

private final AtomicInteger mutableVariableWrapper = new AtomicInteger( 1 ) ;

注意添加了final。这可以防止任何代码用另一个替换我们的AtomicInteger 对象。这种没有volatile 的替换将不是线程安全的。换句话说,我们希望mutableVariableWrapper 本身是可变的。

AtomicInteger 具有用于递增的线程安全方法,例如incrementAndGet。所以这个:

this.mutableVariable++;
System.out.println(this.mutableVariable); 

…变成这样:

int newValue = this. mutableVariableWrapper.incrementAndGet() ;
System.out.println( newValue ); 

请注意上面的那一行,使用AtomicInteger,我们离我们想要的值只有一步之遥。 AtomicInteger 是一个容器,其 int 有效负载嵌套在其中。上面的那一行并没有直接解决该有效负载值。该行要求AtomicInteger 定位有效负载int,并代表我们以线程安全的方式递增它。为了强调远离有效负载这一事实,我在此示例代码中将您的变量名称从 mutableVariable 更改为 mutableVariableWrapper

如果您只想查看当前值,请致电AtomicInteger#get

【讨论】:

  • 很好的答案。谢谢。
【解决方案2】:

您的示例中不需要 volatile。

让我们稍微扩展一下代码:

mainThread@this.mutableVariable = 2;
mainThread@thread.start();
// next line we call only the constructor; not the run method. 
thread@this.blockingQueue.add(new SomeRunnable()) 
mainThread@final Runnable r = this.blockingQueue.take();
mainThread@r.run().this.mutableVariable++;
mainThread@r.run().System.out.println(this.mutableVariable);

只有 1 个线程访问 mutableVariable,即主线程。

这意味着您不需要 volatile,因为单个线程需要表现得好像指令是按顺序执行的一样。

[编辑] Solomon Slow 已经在 cmets 中给出了这个答案。

【讨论】:

    【解决方案3】:

    在这种特定情况下不需要 volatile 关键字。

    让我们调用主线程 T1。

    当您从 T1 调用 thread.start() 时,mutableVariable 的值为 2。

    thread.start() 将在新线程 T2 中执行 this.blockingQueue.add(...)

    r.run() 在 T2 完成运行之前不会被执行,它的整个逻辑是将 Runnable 添加到队列中。

    所以当r.run()被执行时:

    1. this.mutableVariable++ 将 mutableVariable 设置为 3
    2. System.out.println(this.mutableVariable); 将根据需要输出 3。

    【讨论】:

    • 在初始化对象 final Thread thread = new ... 时,mutableVaribale 的值(当前 = 1)不可能在 Thread-2 中“缓存”,因此会产生错误的输出“2”的?
    • Thread-2 直到调用 thread.start 才创建,此时 mutableVaribale 的值已经是 2。不要混淆 Thread object 的创建实际启动不同的操作系统线程。
    • @rranke mutableVariableThread-2 中根本无法访问,因此即使它被缓存(可能是某些内存页面的一部分),也没关系。
    • @OrrBenyamini 你说,mutableVaribale is already 2Thread-2 被创建之前。但是,如果mutableVariable 在调用thread.start 之后 被突变为“2”,会有什么不同吗?!这意味着,如果订单是 thread.start(); this.mutableVariable = 2; 而不是 this.mutableVariable = 2; thread.start(); ?!
    • 在这种情况下,这两行的顺序无关紧要,因为无论如何只能从主线程访问 mutableVariable。 Thread-2 只是将 runnable 添加到阻塞队列中
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-07-26
    • 2018-09-03
    • 2018-01-12
    • 2010-12-09
    • 1970-01-01
    • 2017-02-16
    • 2013-07-19
    相关资源
    最近更新 更多