一、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方法,反序列化会调用,直接返回这个对象就可以了不反序列字节码的那个结果