面试题篇:volatile能否保证线程安全

线程安全涉及可见性、有序性和原子性。volatile保证了可见性和有序性,但无法确保原子性,例如在i++操作中。JIT编译器可能导致优化,使得非volatile变量的更新不被其他线程感知。通过示例展示了volatile如何防止指令重排,从而维护有序性,但单个volatile无法阻止对多个变量的重排序问题。
摘要由CSDN通过智能技术生成

线程安全的考虑方面

线程安全需要考虑3个方面:可见性、有序性、原子性

  • 可见性:一个线程对共享变量修改,另一个线程能看到最新结果
  • 有序性:一个线程内代码按编写顺序执行
  • 原子性:一个线程内多行代码以一个整体运行,期间不能有其它线程的代码插队

volatile能够保证共享变量的可见性有序性,但不能保证原子性

原子性

public static volatile int i = 5;
/**
	伪代码:t1执行i+=5;t2执行i-=5; 
		理想情况下i==5;
*/
t1:i+=5;
t2:i-=5;
/**
	但在jvm底层,i+=5,i-=5会被分解为4步操作
	t1:
	0:getstatic
	3:iconst_5
	4:iadd
	5:putstatic
				t2:
				0:getstatic
                3:iconst_5
                4:isub
                5:putstatic
	在CPU并发运行时,可能会将指令运行为
	t1:
	0:getstatic  //5
				t2:
				0:getstatic  //5
                3:iconst_5   //5
                4:isub		 //0
                5:putstatic  //0
	3:iconst_5  //5
	4:iadd		//10
	5:putstatic	//10
	得到最终i==10,所以volatile不能保证原子性
*/

可见性

// 可见性例子
// -Xint
public class ForeverLoop {
    static volatile boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            get().debug("modify stop to true...");
        },"t1").start();

        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            get().debug("{}", stop);
        },"t2").start();

        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            get().debug("{}", stop);
        },"t3").start();

        foo();
    }

    static void foo() {
        int i = 0;
        while (!stop) {
            i++;
        }
        get().debug("stopped... c:{}", i);
    }
}

解释代码并运行

上述代码里创建了3个线程,t1线程负责休眠100ms后修改共享变量stop=true,让主线程中的foo方法里的循环终止。

不处理可见性问题:如上述代码所示假设stop没有添加volatile关键字,那么运行代码得到结果如下:

[DEBUG] 21:37:39.681 [t2] - true 
[DEBUG] 21:37:39.681 [t3] - true 
[DEBUG] 21:37:39.681 [t1] - modify stop to true... 
....// 循环还在继续

**分析结果:**为什么t2、t3线程获取到的stop为true,而主线程却获取不到t1线程更新后的stop变量呢?

解释原因:这是由于Java中的JIT( Just In Time Compiler 即时编译器)对while(!stop)循环做出了编译优化,当它发现while循环获取到的stop变量在一定阈值之内都是false(我电脑上运行100ms循环了2.5千万次)它就会为了减少CPU与物理内存的频繁读取,就会将原来while(!stop)的字节码改为while(!false)的字节码然后编译成机器码储存起来,后面在执行这行代码它就会将这个机器码交于CPU运行,最终导致主线程中的stop变量无法得到更新,但是物理内存中stop的值已经被修改成true,故t2、t3线程能获取到stop的值为true。

任何一条java代码都是翻译为了字节码指令,但是这个字节码指令并不能直接交给CPU执行,还需要一个解释器的组件,这个解释器会将java字节码指令逐行翻译成机器码交给CPU执行。JIT就是对java中热点的代码(频繁调用的方法,反复执行的循环)进行优化。

如何解决:

  1. 禁止使用JIT(不可取):运行时添加虚拟机参数-Xint即可禁用JIT,但是会影响整个程序的运行效率。JIT优化c1层的优化可以提升5倍的优化,c2层编译器可提升10-100倍的优化。故不可取。
  2. 将变量加上volatile关键字修饰:当JIT发现该变量是由volatile修饰则不会进行优化。

有序性

利用jcstress进行线程压测

情况一:线程不安全。指令重排

@JCStressTest
@Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")// 理想情况就这3种
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")// 指令重排后的情况
@State
public static class Case1 {
    int x;
    int y;

    @Actor
    public void actor1() { // 线程1
        x = 1;
        y = 1;
    }

    @Actor
    public void actor2(II_Result r) { // 线程2
        r.r1 = y;
        r.r2 = x;
    }
}

指令重排后代码执行顺序跟改为:

y = 1;				y = 1;
r.r1 = y; 			r.r2 = x;
r.r2 = x; 			r.r1 = y;
x = 1;				x = 1;

这时代码重排序,造成了1,0这种情况。

情况二:给y加上volatile关键字修饰

此时就不会发生指令重排序,因为volatile关键字修饰的变量会在读操作前加上一个内存屏障,在它之前的指令都不可以越过它先执行,在写操作后也加上一个内存屏障,在它之后的指令都不可以越过它先执行。

情况三:给x加上volatile关键字修饰

此时还是会发生指令重排序,因为给x加上volatile关键字修饰后,y = 1;还是可以在x = 1;之前执行,r.r1 = y;还是可以在r.r2 = x;之后运行。下图所示就会出现1,0的情况。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值