Java内存模型(JMM)和volatile原理

目录

一、Java 内存模型

二、可见性

三、有序性

四、volatile原理

 1、可见性保证

2、有序性保证

五、线程安全的单例


一、Java 内存模型

JMM即Java Memory Model,他定义了主存(共享的数据)工作内存(私有的数据)抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等

JMM体现以下几个方面

  • 原子性-保证指令不会受到线程上下文切换的影响
  • 可见性-保证指令不会受cpu缓存的影响
  • 有序性-保证指令不会受cpu指令并行优化的影响

二、可见性

现在有个run共享变量为true,主线程在sleep,子线程在while循环条件为run为true,然后主线程醒了就run变false,但是我们发现子线程还在运行。

因为Java把内存分为了主存和共享内存

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

因为t要频繁的从出内存中读取run的值,JIT编译器会将run的值缓存到自己工作内存中的高速缓存(工作内存的底层关联了cpu缓存),减少对主存中run的访问,提高效率

1秒后main改了run但是t1线程是从自己的缓存中拿的,所以不知道

解决方案:

volatile(易变关键字)

用来修饰成员变量静态成员变量(局部不可以),就表示这个是变量是从主存中拿取,避免现场从自己工作缓存中查找变量的值,必须到主存中获取,线程操作volatile变量就是直接操作主存

synchronized也可以解决,但是要上monitor操作系统的锁,性能更差

其实在死循环中加入System.out.println也能让停止

    private void newLine() {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.newLine();
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush)
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }

因为print方法加了synchronized关键字(当然println也加了,换行操作也会持有锁)
而synchronized方法也是能保证了该同步块中的变量的可见性的,所以下次stop从主存中读出false就跳出了while。
这里记录一下synchronized做的操作:
1、获得同步锁;
2、清空工作内存;
3、从主内存拷贝对象副本到工作内存;
4、执行代码(计算或者输出等);
5、刷新主内存数据;
6、释放同步锁。
总结一句话:sychronized可以保证变量的可见性

可见性和原子性

前面的例子就是可见性,保证多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,只有一个线程写,多个线程读的时候,这个情况很适合volatile。

synchronized既可以保证原子性,也可以保证可见性。但缺点是synchronized重量级操作,性能相对比较低。

三、有序性

JVM会在不影响正确性的前提下调整语句的执行顺序,这种JVM自己调整执行顺序的操作叫指令重排。但在多线程下指令重拍可能会影响正确性。

指令重排在多线程下可能存在问题

比如上面这种情况,线程2是给num赋值,然后ready为true就唤醒线程1,这个时候如果指令重排了,先执行换标记,然后唤醒了,最后才赋值就会出问题了,他就会直接用0的值

 

想要保证不会出现指令重排就再ready变量加上volatile,他可以保证他前面的指令是顺序执行的

四、volatile原理

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

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

 1、可见性保证

写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存中。这样别的线程就都能看到最终同步到主存的数据,从而避免了指令重排的问题。

而读屏障保证在该屏障后,对共享变量的读取,加载的是主存中的最新数据,意思是读取的时候不会读自己本地的,而是去主存中读取

2、有序性保证

写屏障会保证指令重排的时候,不会把写屏障之前的代码排在写屏障之后,他前面的一定是在他前面写入主存的。

读屏障会保证指令重排的时候,不会让读屏障之后的代码排在读屏障的前面先读

注意:有序性的保证只能保证本线程内相关代码不被重排序

五、线程安全的单例

判断是否为null和new这个步骤可能有线程安全问题,所以要加syn,他是进来先判断有没有的,没有才创建,所以也叫懒惰初始化

但是这样做可能有问题,就是我每次都要加syn进来判断是不是为null,这个上锁是有开销的,如果第一次创建成功后,后面的判断都是不成功还要加锁很浪费性能

这个时候我们就可以用双重判断,外面加一个if来判断是不是null,如果不是null都不用加锁了

但是这样可以有问题:指令重排的问题,在字节码文件的时候有空new对象构造器构造和给INSTANCE赋值的顺序变化,如果先赋值但是还构造的时候,别人判断到这个对象已经被赋值了不为null所以直接return这个没构造好的对象了

虽然说synchronized可以保证对象的有序性,但是前提是要让synchronized完全管理这个对象,但是现在这个INSTANCE这个对象是放到外面的,synchronized只是赋值

解决方案:

其实就是在INSTANCE这个变量上加个volatile就行了

因为volatile会加写屏障,他可以保证他之前的指令一定在前面执行,所以这个赋值前必须先构造,所以赋值的时候一定被构造了。

问题:

单例的类为什么要加final?

因为final的类不能别继承,如果子类实现了这个类然后重写方法,重写可能会破坏单例

如果实现了序列化接口,还要做什么防止返序列化来破坏单例?

对象的创建不一定是通过new来创建的,如果我们实现了序列化接口,我们反序列化的时候,也会创建新对象,其实解决就是加上一个readResovle方法,反序列化会调用,直接返回这个对象就可以了不反序列字节码的那个结果

猜你喜欢

转载自blog.csdn.net/weixin_54232666/article/details/131236307