并发编程的故事——共享模型之内存

共享模型之内存


一、JVM内存抽象模型

主要就是把cpu下面的缓存、内存、磁盘等抽象成主存和工作内存
体现在
可见性
原子性
有序性

二、可见性

出现的问题
t线程如果频繁读取一个静态变量,那么JIT编译器会把它存入到线程的缓存,那么就算主线程修改了主存中的静态变量也没有任何作用,因为t线程读取的是缓存里面的。所以程序判断仍然是错误无法停止。

@Slf4j(topic = "c.Test32")
public class Test32 {
    
    
    // 易变
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t = new Thread(()->{
    
    
            while(true){
    
    
                    if(!run) {
    
    
                        break;
                    }
            }
        });
        t.start();

        sleep(1);
            run = false; // 线程t不会如预想的停下来
    }
}

在这里插入图片描述
解决方案
volatile和synchronized可以让线程不能访问缓存,一定要访问主内存里面的run。

@Slf4j(topic = "c.Test32")
public class Test32 {
    
    
    // 易变
     static boolean run = true;
     static Object lock=new Object();

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t = new Thread(()->{
    
    
            while(true){
    
    
                synchronized (lock){
    
    
                    if(!run) {
    
    
                        break;
                    }
                }

            }
        });
        t.start();

        sleep(1);
        synchronized (lock){
    
    
            run = false; 
        }

    }
}

加上sout也是可以解决可视化问题。原因是这个println是一个synchronize的方法,也就是要输出那么就会在同步块,同步块可以完成可视化,那么自然run就可以被读取。

public void println(boolean x) {
    
    
        synchronized (this) {
    
    
            print(x);
            newLine();
        }
    }

三、指令重排序

为什么要指令重排?
因为各个语句都是由多个指令组成,相当于是多个分工,这些分工有的可以同时完成,那么就把他们先组合到一起。其它需要前一步的结果的指令就在后面排序等待。

诡异的结果
这里其实就是指令重排会导致这个结果是0。实际上就是线程2指令重排先执行了ready=true,然后被切换到线程1,刚好通过if先做出了计算,最后才是切换到线程2执行num=2
在这里插入图片描述
解决
使用volatile可以防止变量前面的代码重排序
在这里插入图片描述
volatile原理
volatile的原理其实就是内存屏障。写屏障就是把修改的变量之前的所有变量同步到主存中每次都是在主存中修改,而且防止前面的代码指令重排到屏障之后。如果是读屏障那么就是带有volatile变量以下的所有变量都同步到主存中,防止屏障以下的代码重排到屏障之前,也就保护了volatile属性。
保证了写屏障的变量是最新的
但是无法解决指令交错问题,也就是只能在本地线程保证指令有序,但是无法保证多线程的指令交错问题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
双重检查
单例模式为了防止多次加锁,可以先判空之后,再加锁,再判空。这样的好处就是创建对象之后,只需要判空,而不需要再次加锁。只有在第一次需要加锁创建对象,防止多个线程同时创建对象。
问题
第一个if代码会被指令重排,为什么会重排?
在这里插入图片描述
双重检查的问题根源分析(dcl)
关键就是if(INSTANCE==null)是一个在monitor之外的代码,那么产生的问题就是在执行INSTANCE=new Singleton()的时候,他并不是一个原子操作,包括了invokespecial执行构造方法指令和putstatic给引用赋值(找到对象的堆内存地址)
补充:那么这里synchronize为什么还是会出现指令重排?
原因是它本来就会产生指令重排,只不过在synchronize中不会产生原子化,可视化和有序化的问题,但这里是两个线程而且synchronize没有完全控制变量INSTANCE的原因。
在这里插入图片描述
在这里插入图片描述
解决方案
可以通过volatile的读写屏障防止代码指令重排到屏障之外,这样就能够避免invokespecial走到putstatic后。
在这里插入图片描述
在这里插入图片描述
happen-before(可见性)
synchronize
volatile
等待线程执行完之后再读取变量
静态变量写好之后,线程才调用
线程打断之前的修改
变量默认值
在这里插入图片描述
在这里插入图片描述
习题
balking习题
指令重排序问题,解决方案可以使用synchronize来把这些变量框住,防止其它线程切换的时候都通过了第一个if,导致重复执行问题
在这里插入图片描述
在这里插入图片描述
1、为什么加上final
原因就是防止类被继承,之后重写的方法带上单例对象被改变
2、怎么防止反序列化破坏单例
需要增加一个返回Obj的方法,直接返回单例对象,而不是通过字节码重新创建
3、为什么构造私有化
防止被创建很多次
4、初始化能保证线程安全吗
静态变量在类加载的时候完成了初始化
5、不把Instance变成public的原因
防止直接被修改,提供封装性,隐藏细节

在这里插入图片描述
1、字节码里面全部都是public final static的类对象,所以可以限制实例对象
2、不会有并发问题,在类加载的时候静态变量已经加载完了
3、不会被反射破坏单例,enum的设计
4、也不会被反序列化破坏,它实现了序列化和返回单例的方法
5、它是一个饿汉式
在这里插入图片描述
在这里插入图片描述
总结
可见性(jvm优化速度,把变量放进线程的缓存)
有序性(指令重排,优化执行速度)
happen-before写入是否对线程可见
volatile原理
同步模式balking


猜你喜欢

转载自blog.csdn.net/weixin_45841848/article/details/132614338
今日推荐