并发线程基础第五篇

目录

共享模型之内存

Java内存模型

可见性

停不下的循环

解决办法

 有序性

为什么会有指令重排这种优化呢?

多线程下怎么解决这种指令重排问题带来的影响呢?

volatile原理

double-checked locking 问题

以著名的 double-checked locking 单例模式为例

double-checked locking 解决

happens-before


共享模型之内存

Java内存模型

Java内存模型(Java Memory Model,JMM),它定义了主存,工作内存抽象概念,底层对应着cpu寄存器,缓存,硬件内存,cpu指令优化。

JVM体现在以下几个方面

原子性-保证指令不会受到上下文切换的影响

可见性-保证指令不会受cpu缓存的影响

有序性-保证指令不会受cpu指令并行优化的影响

可 见 性 - 由 J V M 缓 存 优 化 引 起

有 序 性 - 由 J V M 指 令 重 排 序 优 化 引 起

可见性

停不下的循环

@Slf4j
public class Test15 {
    static boolean b =true;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (b){
                //代码块
            }
        });
        thread.start();
        Thread.sleep(1000);
        log.debug("将变量置为false");
        b=false;
    }
}

上面示例,将变量置为false之后,线程thread还一直在运行,而没有停下来。下面我们分析一下原因。

1.初始状态,t线程刚开始从主内存读取了run的值到工作内存

 

2.因为t线程要频繁从主内存读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率

 

 3.一秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

解决办法

volatile(易变关键字)

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

它不能保证原子性,仅用在一个写线程,多个读线程的情况

 有序性

JVM会在不影响正确性的前提下,可以调整语句的执行顺序,这种特性称之为指令重排,多线程情况下指令重排会影响其正确性。

为什么会有指令重排这种优化呢?

指令重排是现代处理器为了提高执行效率而采取的一种优化手段。虽然指令重排可能会导致程序的执行顺序与代码编写顺序不一致,但在不影响程序语义的前提下,可以带来性能的提升。

指令重排可以充分利用处理器的并行执行能力和流水线技术,使得处理器能够更快地执行指令,从而提高程序的执行速度。

多线程下怎么解决这种指令重排问题带来的影响呢?

对一个volatile变量的写操作 happens-before 于后续对这个volatile变量的读操作。这确保了对volatile变量的写入操作对所有线程可见,并且禁止了volatile变量之前的操作与volatile变量之后的操作重排序。所以在变量之前加上volatile关键字即可

volatile原理

volatile的底层实现原理是内存屏障

  • 对volatile变量的写指令后会加入写屏障
  • 对volatile变量的读指令前会加入读屏障

如何保证可见性

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

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

如何保证有序性

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

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

double-checked locking 问题

以著名的 double-checked locking 单例模式为例

public final class Single {
    private Single(){}
    private static Single instace = null;
    public static Single getInstace(){
        if(instace == null){
            synchronized (Single.class){
                if(instace == null){
                    instace = new Single();
                }
            }
        }
        return instace;
    }
}

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外但在多线程环境下,上面的代码是有问题的

关键在于 第一个if,这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值

这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初 始化完毕的单例

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效  

double-checked locking 解决

public final class Single {
    private Single(){}
    private static volatile Single  instace = null;
    public static Single getInstace(){
        if(instace == null){
            //实例没有创建,才会进入内部的synchronized代码块
            synchronized (Single.class){
                //也许其他线程已经创建实例,所以再判断一次
                if(instace == null){
                    instace = new Single();
                }
            }
        }
        return instace;
    }
}

如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),

保证下面 两点:

可见性

  • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
  • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据

有序性

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

happens-before

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

线程解锁 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();

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

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

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

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

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

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

线程 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);
}

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

  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值