1. 内存模型

Java内存模型是Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。
简单来说JMM定义了一套在多线程读写共享数据时,对数据可见性、有序性和原子性的规则和保障。

1.1 原子性

synchronized同步关键字。

/**
 * @Description JMM——原子性
 * @date 2022/3/30 8:39
 */
public class Atomicity {
    static int i = 0;

    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int j = 0; j < 50000; j++) {
                synchronized (obj){
                    i++;
                }
            }
        });
        
        Thread t2 = new Thread(()->{
            for (int j = 0; j < 50000; j++) {
                synchronized (obj){
                    i--;
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
    }
}

使用synchronized关键字之后,当前对象的monitor区域能够使用,其中包括:

  1. Owner:只能有一个线程成为Owner
  2. EntryList:排队等候区,其它线程成为Owner,再进来线程就要在这等待(阻塞),等到Owner执行完毕后,EntryList中的线程就会争抢成为新的Owner
    在这里插入图片描述

1.2 可见性

下方代码中:运行一个t线程从主线程中读取run变量,等待线程睡眠一秒后,修改run的值,但t线程并不会停下来。

/**
 * @Description JMM——可见性
 * @date 2022/3/30 8:55
 */
public class Visibility {
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
           while (run){

           }
        });
        t.start();
        Thread.sleep(1000);
        run = false;
    }
}

内存模型:
在这里插入图片描述
由于t线程要频繁的读取run的值,即时编译器会将run缓存到工作内存中,减少对主线程的访问,提高效率:
在这里插入图片描述
1秒之后修改的run的值也仅仅是在主线程中修改的,但高速缓存没有变化。
在这里插入图片描述

volatile关键字,用来修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主内存中获取它的值。但是它只能保证一个线程写入,其它线程读取的情况。如果多个线程一起写入就无法保证变量的原子性。

1.3 有序性

指令重排:是即使编译器在运行时的优化,JVM会在不影响正确性的前提下,可以调整语句的执行顺序。但是在多线程的情况下,指令重排会影响正确性。使用volatile关键字可以避免指令重排。

happen-before:如果一个操作执行的结果需要对另一个操作可见。

规则

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

规则原则: 摘自《深入理解Java虚拟机第12章》)

  • 程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。

  • volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。

  • 传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C

  • 线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。

  • 线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。

1.4 CAS与原子类

CAS(Compare and Swap):体现一种乐观锁的思想,它会不断的尝试进行某种操作,直到当前线程修改的同时对其它的线程没有影响。获取变量时,为保证可见性一般使用volatile来修饰。因为没有使用synchronized关键字,不会造成线程阻塞,效率会提高。但如果竞争激烈,重试会频繁发生,效率会收到影响。一般工作在多核CPU下。

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

Java中的乐观锁和悲观锁:

  1. 乐观锁:CAS就是乐观锁的思想,最乐观的估计,不怕别的线程来修改共享变量,即使改了之后,CAS也会在重试。
  2. 悲观锁:synchronized是悲观锁的思想,最悲观的估计,防着其它线程来修改共享变量,只有等待解卡锁之后其它的线程才有机会。

1.5 原子类

Java中的JUC提供了原子操作类,提供线程安全的操作,底层使用CAS + volatile来实现。

  1. 基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
  2. 数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
  3. 引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
  4. 对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater

1.6 synchronized优化

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

1.6.1 轻量级锁

如果一个对象虽然有多线程访问,但是多线程访问时间是错开的,就可以使用轻量级锁。

两个代码块使用一个对象加锁

/**
 * @Description synchronized 优化 —— 轻量级锁
 * @date 2022/3/30 15:11
 */
public class LightLock {
    static Object obj = new Object();

    public static void method1(){
        synchronized (obj){
            method2();
        }
    }

    public static void method2(){
        synchronized (obj){

        }
    }
}

加锁过程:每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark
word。
在这里插入图片描述

1.6.2 锁膨胀

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

在这里插入图片描述

1.6.3 重量级锁

重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功,当前线程就可以避免阻塞。
在Java6之后自旋锁是自适应的,如果一个对象刚刚自旋成功,那么就认为本次自旋的成功率高,会多自旋几次,反正就少自旋。

特点:

  • 多核CPU自旋才能发挥优势;
  • Java7之后不能控制是否开启自旋功能。

自旋重试成功:
在这里插入图片描述

自旋重试失败:
在这里插入图片描述
所谓自旋成功就是碰巧了在重试的时候碰到正在执行同步代码的线程刚好完成,但如果重试多次失败就会放弃重试进入阻塞状态。

1.6.4 偏向锁

轻量级锁在没有竞时,每次重入仍然需要执行CAS操作。Java6引入了偏向锁:只有第一次使用CAS将线程ID设置到对象的Mark word头,之后线程如果ID是自己的就表示没有竞争,不需要重新CAS。

特点

  • 撤销时需要将持锁线程升级为轻量锁,过程需要Stop the world
  • 访问对象的hashCode也会撤销偏向锁;
  • 如果对象被多个线程访问,没有竞争,偏向了T1线程的对象仍有可能偏偏向T2线程,重新偏向会重置对象的Thread ID;
  • 撤销和重偏向都是以类为单位批量进行的;
  • 当撤销偏向达到某个阈值,整个类的所有对象都会变为不可偏向的。
1.6.5 其它优化
  1. 减少上锁时间:同步代码块尽量短;
  2. 减少锁的粒度:将一个锁拆分为多个锁提高并发;
  3. 锁粗化:多次循环进入同步块不如同步块内多次循环;
  4. 锁消除:当对象不会被其它先访问到,这是即时编译器就会忽略掉所有同步操作。
  5. 读写分离:同步写操作。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐