JVM----④内存模型

内存模型

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

发布了138 篇原创文章 · 获赞 3 · 访问量 7246

猜你喜欢

转载自blog.csdn.net/weixin_43719015/article/details/104887000