java 内存模型 JMM

JMM定义了一套在多线程读写共享数据时(成员变量,数组)时,对数据的 可见性、原子性和有序性 的规则和保障。

image.png

1 java内存模型

1.1 原子性

Java对静态变量的自增或者自减(i++,i--)不是原子操作。
i++的字节码指令为:(i为静态变量,局部变量的话不一样)

getstatic   i       //获取静态变量i的值
iconst_1            //准备常量1
iadd                //自增
putstatic   i       //将修改后的值存入静态变量i

i--的字节码指令为:

getstatic   i       //获取静态变量i的值
iconst_1            //准备常量1
isub                //自减
putstatic   i       //将修改后的值存入静态变量i

java的内存模型如下,完成静态变量的自增或者自减,需要在主存和线程内存中进行数据交换。由于多个线程按照时间片轮流使用cpu,会导致切换的时候值为脏值。

image.png

1.2 可见性

public class Demo4_2 {
    static boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (run) {}
        }, "t").start();
        TimeUnit.SECONDS.sleep(1);
        run = false;
    }
}

休眠1秒后run置为false后并没有让线程停止,这是由于:

  1. 初始状态,t线程刚开始从主内存读取了run的值加载到工作内存。
image.png
  1. 因为t线程要频繁从主存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率。
image.png
  1. 1秒之后,main线程修改了run值,并同步至主存,而t是从自己的工作内存中的告诉缓存中读取run,结果永远是旧值。
image.png
  1. 解决方式:
  • 可以通过添加volatile(保证可见性,不保证原子性,禁止指令重排)解决。
  • 在while中加入System.out.println();也能保证结束,这是由于输出语句底层采用了synchronized关键字,保证了原子性与可见性,强制了t线程从主存中读取。

1.3 有序性

主要是因为存在 指令重排,同样可以通过volatile来禁止指令重排。

2 CAS与原子类

2.1 CAS

CAS即 Compare and Swap,体现乐观锁的思想。

image.png

获取共享变量时,为了保证变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核CPU的场景下。

  • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,重试必然频繁发生,反而效率会受影响

CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令。

2.2 乐观锁与悲观锁

java中的乐观锁其实就是CAS,悲观锁就是synchronized。

  • CAS集于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没有关系,我吃亏点在重试。
  • synchronized是基于悲观锁的思想:最悲观的估计,得放着其他线程来修改共享变量,我上了锁你们都别想改,我改完了释放锁你们才能改。

2.3 原子操作类

juc(java.until.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,它们底层就是采用CAS技术+volatile来实现的。

但是CAS会存在ABA问题。

3 synchronized

java HotSpot虚拟机中,每个对下你个都有对象头(包括class指针和Mark Word)。Mark Word平时存储该对象的 哈希码、分代年龄,当加锁时,这些信息就根据情况被替换为 标记位、线程锁记录指针、重量级锁指针、线程ID等内容。

3.1 轻量级锁

多个线程交替执行,不存在竞争,那么可以使用轻量级锁进行优化。

3.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

3.3 重量锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用CPU时间,单核CPU自旋就是浪费时间,多核CPU自旋才能发挥优势。
  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)。
  • Java7后不能控制是否开启自旋功能。

3.4 偏向锁

轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作。Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。

  • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
  • 访问对象的hashCode也会撤销偏向锁
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
  • 撤销偏向和重偏向都是批量进行的,以类为单位
  • 如果撤销偏向达到某个阈值,整个类的所有对象都会变为不可偏向的
  • 可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁

3.5 其他优化

3.5.1 减少上锁时间

同步代码块中尽量短

3.5.2 减少锁的粒度

将一个锁拆分为多个锁提高并发度,如:

  • ConcurrentHashMap
  • LongAdder 分为base和cells两部分。没有并发争用的时候或者是cells数组正在初始化的时候,会使用CAS来累加值到base,有并发争用,会初始化cells数组,数组有多好个cell,就允许有多少线程并行修改,最后将数组中每个cell累加,在加上base就是最终的值
  • LinkedBlockingQueue 入队和出队使用不同的锁,想对于LinkedBlockingArray只有一个锁效率更高

3.5.3 锁粗化

多次循环进入同步块不如同步块内多次循环
另外JVM可能会做如下优化,把多次append的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)

new StringBuffer().append("a").append("b").append("c");

3.5.4 锁消除

JVM会进行代码的逃逸分析,例如某个加锁对象是方法内的局部变量,不会被其他线程所访问到,这时候就会被即时编译器忽略掉所有的同步操作。

3.5.5 读写分离

CopyOnWriteArrayList
ConvOnWriteSet

你可能感兴趣的:(java 内存模型 JMM)