并发——volatile

保证可见性

看一个现象:


 
 

static boolean run = true;
 
public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()->{
            while(run){
                        
                     // ....        
           }   
    });    
    t.start();
 
    sleep(1);    
    run = false; // 线程t不会如预想的停下来 

main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
原因:

  • 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率
  • 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值
    在这里插入图片描述

解决方法:

volatile(易变关键字) 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取
它的值,线程操作 volatile 变量都是直接操作主存

原理:

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中新数据

要注意的是:原子性和可见性的区别:
volatile比较适合一个线程修改,多个线程读取的操作。并不能保证原子性
对于线程切换的指令交错现象依然会产生错误。

保证有序性

起因:JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,这种特性又被称为指令重排。

如下代码:


int num = 0;
boolean ready = false;
 
// 线程1 执行此方法 
public void actor1(I_Result r) { 
   if(ready) {        
   		r.r1 = num + num;    
   } else { 
        r.r1 = 1;    
   } 
}
 
// 线程2 执行此方法 
public void actor2(I_Result r) {
       num = 2;    
       ready = true;    
 }

产生的结果会有

1.线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
2.线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
3.线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
4.线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

解决方法:

volatile 修饰的变量,可以禁用指令重排

原理:
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
.

happens-before

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛 开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

1.线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见:

static int x; 
static Object m = new Object();
 
new Thread(()->{
    synchronized(m) {
            x = 10;


	} 
},"t1").start();
 
new Thread(()->{    
	synchronized(m) {        
		System.out.println(x);    
	} 
},"t2").start();

2.线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

volatile static int x;
 
new Thread(()->{   
 	x = 10; 
 },"t1").start();
 
new Thread(()->{
    System.out.println(x); 
},"t2").start();

3.线程 start 前对变量的写,对该线程开始后对该变量的读可见

static int x;
 
x = 10;
 
new Thread(()->{
    System.out.println(x); 
},"t2").start()

4.线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待 它结束)

static int x;
 
Thread t1 = new Thread(()->{
    x = 10; 
},"t1"); t1.start();
 
t1.join(); 
System.out.println(x);

5.线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)


static int x;
 
public static void main(String[] args) {
    Thread t2 = new Thread(()->{
            while(true) {
                  if(Thread.currentThread().isInterrupted()) {
                                  System.out.println(x);
                                  break; 
                   }        
            }
     },"t2");   
     t2.start();
 
    new Thread(()->{
	     sleep(1);
	     x = 10;
	     t2.interrupt();
     },"t1").start();
 
    while(!t2.isInterrupted()) {
            Thread.yield();    
    }
    System.out.println(x); }

6.对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

7.具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子

volatile static int x; 
static int y;
 
new Thread(()->{
    y = 10;
    x = 20; 
},"t1").start();
 
new Thread(()->{
    // x=20 对 t2 可见, 同时 y=10 也对 t2 可见    
    System.out.println(x); 
},"t2").start();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值