内存模型
java 内存模型
1.原子性,2.可见性,3.有序性,4.CAS与原子类,5.synchronized 优化
很多人将【java 内存结构】与【java 内存模型】傻傻分不清,【java 内存模型】是 Java MemoryModel(JMM)的意思。
简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障
1.原子性
1.1 原子性案例
问题提出,两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
1.2 问题分析
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和线程内存中进行数据交换:
1.3 解决方法
synchronized(同步关键字)
语法
synchronized( 对象 ) {
要作为原子操作代码
}
说明: 一个对象而言,同一时刻只能有一个线程持有Owner,发现Owner为空,有一个t1线程就会成为Owner,并且用monitor enter 指令对monitor进行锁定,如果有t2来了,发现Owner有人了,就会进入到EntryList中,排队等候区,会阻塞住,当t1执行完毕后,monitor exit指令,通知EntryList可以过来了,进行挣抢;WaitSet 是wait notify方法时发生的;
2. 可见性
先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
public class Demo4_2 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
// System.out.println(1);
}
});
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
}
2.2 解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
2.3 可见性
注意synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低
如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?
3. 有序性
3.1 诡异的结果
结果还有可能是 0
借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress
3.2 解决方法
3.3 有序性理解
这种特性称之为『指令重排』,**多线程下『指令重排』**会影响正确性,例如著名的 double-checked locking 模式实现单例
以上的实现特点是:
3.4 happens-before
happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z
4. CAS 与原子类
4.1 CAS
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,下面是直接使用 Unsafe 对象进行线程安全保护的一个例子
4.2 乐观锁与悲观锁
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
4.3 原子操作类
5. synchronized 优化
当给对象加锁后,对象头信息就会被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等内容
5.1 轻量级锁
每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
Mark Word 8个字节,当给对象加锁时,就会将对象头信息存储到线程的栈帧的锁记录,做完之后又给它恢复回去,换回去。类似于线程和Mark Word做了名片的交换,将来解锁再换回来;如果说set进去了表示加锁成功,如果没成功就说明有竞争,需要升级。
5.2 锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
5.3 重量锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)
- Java 7 之后不能控制是否开启自旋功能
5.4 偏向锁
5.5 其它优化
1. 减少上锁时间
同步代码块中尽量短
2. 减少锁的粒度
将一个锁拆分为多个锁提高并发度,例如:
2. 锁粗化
多次循环进入同步块不如同步块内多次循环
另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)
newStringBuffer().append("a").append("b").append("c");
4. 锁消除
JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。
5. 读写分离
CopyOnWriteArrayList
ConyOnWriteSet
参考:
https://wiki.openjdk.java.net/display/HotSpot/Synchronization
http://luojinping.com/2015/07/09/java锁优化/
https://www.infoq.cn/article/java-se-16-synchronized
https://www.jianshu.com/p/9932047a89be
https://www.cnblogs.com/sheeva/p/6366782.html
https://stackoverflow.com/questions/46312817/does-java-ever-rebias-an-individual-lock