深入 Java 并发锁

如果只有一个线程来执行代码,那么就不会有并发的问题。但是,现在 cpu 都是多核心,更别说一个核心(处理器)也可以通过切换时间片来并发的执行代码。这样就会出现数据的错乱问题,所以需要处理代码的执行顺序,来解决问题。

现实中很有多关于并发的问题,比如:

12306卖票,既要保证票的正确性,不多卖也不少买,又要代码执行效率高。

在深入原理之前,我们应该首先了解 Java内存模型。

1. Java内存模型(JMM)

在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型。

1.1 计算机结构

1.1.1 计算机结构简介

冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备存储器,控制器,运算器。

1422833-20180701221846878-376922278

CPU

中央处理器,是计算机的控制和运算的核心,我们的程序最终都会变成指令让CPU去执行,处理程序中的数据。

20200217122701961

内存

我们的程序都是在内存中运行的,内存会保存程序运行时的数据,供CPU处理。

“内存”的图片搜索结果

缓存

CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。最靠近CPU的缓存称为L1,然后依次是 L2,L3和主内存,CPU缓存模型如图下图所示。

“cpu 缓存结构”的图片搜索结果

CPU Cache 分成了三个级别: L1, L2, L3。级别越小越接近CPU,速度也更快,同时也代表着容量越小。

  1. L1是最接近CPU的,它容量最小,例如32K,速度最快,每个核上都有一个L1 Cache。
  2. L2 Cache 更大一些,例如256K,速度要慢一些,一般情况下每个核上都有一个独立的L2 Cache。
  3. L3 Cache是三级缓存中最大的一级,例如12MB,同时也是缓存中最慢的一级,在同一个CPU插槽
    之间的核共享一个L3 Cache。

cachemem.png

Cache 的出现是为了解决CPU直接访问内存效率低下问题的,程序在运行的过程中,CPU接收到指令后,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,如果命中缓存,CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写人,当运算结束之后,再将CPUCache中的最新数据刷新到主内存当中,CPU通过直接访问Cache的方式替代直接访问主存的方式极大地提高了CPU 的吞吐能力。但是由于一级缓存(L1 Cache)容量较小,所以不可能每次都命中。这时CPU会继续向下一级的二级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3Cache、内存(主存)和硬盘。

1.1.2 小结

计算机的主要组成CPU,内存,输入设备,输出设备。

1.2 Java 内存模型

1.2.1 Java内存模型的概念

Java Memory Molde (Java 内存模型),千万不要和Java内存结构混淆。

Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

image-20200218145347519

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。

  • 主内存

    主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

  • 工作内存

    每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量

1.2.2 Java内存模型的作用

Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。

1.2.3 CPU缓存,内存与Java内存模型的关系

通过对前面的 CPU硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行。

但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存和主内存之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。

JMM内存模型与CPU硬件内存架构的关系:

image-20200218145507860

1.2.4 小结

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。

1.3 主内存与工作内存之间的交互

1.3.1 交互过程

Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

对应如下的流程图:

image-20200218145215318

1.3.2 内存间交互操作
  1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。
  4. load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。
  6. assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。
  7. store(存储):把工作内存的变量的值传递给主内存。
  8. write(写入):把store操作的值入到主内存的变量中。
1.3.3 注意
  1. 不允许read、load、store、write操作之一单独出现
  2. 不允许一个线程丢弃assgin操作
  3. 不允许一个线程不经过assgin操作,就把工作内存中的值同步到主内存中
  4. 一个新的变量只能在主内存中生成
  5. 一个变量同一时刻只允许一条线程对其进行lock操作。但lock操作可以被同一条线程执行多次,只有执行相同次数的unlock操作,变量才会解锁
  6. 如果对一个变量进行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assgin操作初始化变量的值
  7. 如果一个变量没有被锁定,不允许对其执行unlock操作,也不允许unlock一个被其他线程锁定的变量
  8. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值
  9. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中
1.3.4 小结

主内存与工作内存之间的数据交互过程

lock -> read -> load -> use -> assign -> store -> write -> unlock

synchronized用的是lock 和unlock。

2. 并发编程的三个问题

如果有多个线程来操作共享数据,很有可能共享数据的值会出现错乱,导致线程安全的根本原因,就是

  • 原子性
  • 有序性
  • 可见性

2.1 原子性

原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。

2.1.1 原子性演示

案例演示:5个线程各执行1000次 i++

/**
 * 目标:演示原子性问题
 * 1.定义一个共享变量number
 * 2.对number进行1000的++操作
 * 3.使用5个线程来进行
 */
public class TestAtomicity {

    // 1.定义一个共享变量number
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        // 2.对number进行1000的++操作
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                number++;
            }
        };

        List<Thread> list = new ArrayList<>();
        // 3.使用5个线程来进行
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }

        for (Thread t : list) {
            t.join();
        }

        System.out.println("number = " + number);
    }
}

运行结果如下,结果不固定,预期值是 5000

number = 4877

使用 javap反汇编class文件,得到下面的字节码指令:

javap -p -v class文件

image-20200217120206508

其中,对于 number++ 而言(number 为静态变量),实际会产生如下的 JVM 字节码指令:

9:  getstatic   #18         // Field number:I
12: iconst_1
13: iadd
14: putstatic   #18         // Field number:I

由此可见 number++是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出现问题。比如一个线程在执行13: iadd时,另一个线程又执行9: getstatic。会导致两次number++,实际上只加了1。

2.1.2 另一个例子

两个线程对初始化为 0 的静态变量,一个做自增,一个做自减,各种 5000 次,结果是 0 吗?

public class Demo {
    private static int i = 0;
    private static final Object obj = new Object();

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

        t1.join();
        t2.join();
        System.out.println(i);
    }
}

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

 8: getstatic     #9                  // 获取静态变量i的值
11: iconst_1													// 准备常量1
12: iadd															// 加法
13: putstatic     #9                  // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

 8: getstatic     #9                  // 获取静态变量i的值
11: iconst_1													// 准备常量1
12: isub															// 减法
13: putstatic     #9                  // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和线程内存中进行数据交换:

image-20200218145347519

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

 8: getstatic     #9                  // 获取静态变量i的值
11: iconst_1													// 准备常量1
12: isub															// 减法
13: putstatic     #9                  // 将修改后的值存入静态变量i
2.1.3 解决方法

synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

synchronized (锁对象) {
    // 受保护资源;
}

两个列子的通过synchronized解决上面的问题。

public class TestAtomicity {

    // 1.定义一个共享变量number
    private static int number = 0;

    private static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 2.对number进行1000的++操作
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (obj) {
                    number++;
                }
            }
        };

        List<Thread> list = new ArrayList<>();
        // 3.使用5个线程来进行
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }

        for (Thread t : list) {
            t.join();
        }

        System.out.println("number = " + number);//number = 5000
    }
}

另一个

public class Demo {

    private static int i = 0;

    private static final Object obj = new Object();

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

        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                for (int j = 0; j < 50000; j++) {
                    i--;
                }
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(i);
    }
}

如何理解呢:你可以把 obj 想象成一个房间,线程 t1,t2 想象成两个人。

当线程 t1执行到synchronized (obj)时,就好比t1 进入了这个房间,并反手锁住了们,在门内执行 number++代码。

这时候如果t2 也运行到synchronized (obj)时,它发现门被锁住了,只能在门外等待。

当 t1 执行完synchronized{}块内的代码,这时候才会解开们上的锁,从 obj房间出来。t2 线程这时才可以进入 obj 房间,反锁住门,执行它的number++。

注意:例子中t1 和t2 线程必须用 synchronized 锁住同一个 obj 对象,如果 t1 锁住的是 m1 对象,t2 锁住的是 m2 对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果。

2.1.4 小结

并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。

synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

2.2 可见性问题

可见性(Visibility):是指一个线程对共享变量进行修改,另一个线程立即得到修改后的最新值。

2.2.1 案例演示

先来看一个现象,main 线程对 flag 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

/**
 * 目标:演示可见性问题
 * 1.创建一个共享变量
 * 2.创建一条线程不断读取共享变量
 * 3.修改共享变量
 */
public class Test01Visibility {

    // 1.创建一个共享变量
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        // 2.创建一条线程t不断读取共享变量
        Thread t = new Thread(() -> {
            while (flag) { // ....

            }
        });
        t.start();

        Thread.sleep(1000);
        //3.修改共享变量
        flag = false; // 线程t不会如预想的停下来
    }
}
2.2.2 分析问题

为什么呢?分析一下:

  1. 初始状态,线程刚开始从主内存读取了 flag 的值到工作内存。

    image-20200217155635575

  2. 因为 t 线程要频繁从主内存中读取 flag 的值,JIT 编译器会将 flag 的值缓存至自己工作内存中的高速缓存中,减少对主存中 flag 的访问,提高效率

    image-20200217155737985

  3. 1秒之后,main 线程修改了 flag 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

    image-20200217155813710

2.2.3 volatile 保证可见性

volatile(关键字):仅用在一个写线程,多个读线程的情况

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

public class TestVisibility {

    // 1.创建一个共享变量 添加 volatile 
    private static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        // 2.创建一条线程t不断读取共享变量
        Thread t = new Thread(() -> {
            while (flag) { // ....

            }
        });
        t.start();

        Thread.sleep(1000);
        //3.修改共享变量
        flag = false; // 线程t不会如预想的停下来
    }
}

可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:

getstatic  flag  // 线程 t 获取 flag true
getstatic  flag  // 线程 t 获取 flag true
getstatic  flag  // 线程 t 获取 flag true
getstatic  flag  // 线程 t 获取 flag true
putstatic  flag  // 线程 main 修改 flag 为 false, 仅此一次
getstatic  flag  // 线程 t 获取 flag false

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错

// 假设i的初始值为0 
getstatic i  // 线程1-获取静态变量i的值 线程内i=0 
getstatic i  // 线程2-获取静态变量i的值 线程内i=0
iconst_1 	 // 线程1-准备常量1
iadd 		 // 线程1-自增 线程内i=1 
putstatic i  // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 	 // 线程2-准备常量1
isub 		 // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1    

说明 volatile 只适用于一个写线程,多个读线程的情况。

2.2.4 synchronized保证可见性

案例演示:一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,另一个线程并不会停止循环。

public class TestVisibility {

    // 1.创建一个共享变量 
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        // 2.创建一条线程t不断读取共享变量
        Thread t = new Thread(() -> {
            while (flag) {
                System.out.println(1);//
            }
        });
        t.start();

        Thread.sleep(1);
        //3.修改共享变量
        flag = false; // 线程t会停下来
    }
}

//System.out.println 源码,在代码里面有 synchronized 
public void println(int x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}
2.2.5 synchronized保证可见性的原理

image-20200218145215318

2.2.6 小结

synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值。

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低。

2.3 有序性

有序性(Ordering):是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

public static void main(String[] args) {
    int a = 10;
    int b = 20;
}
2.3.1 有序性演示

jcstress是java并发压测工具。

修改pom文件,添加依赖:

<dependencies>
    <dependency>
        <groupId>org.openjdk.jcstress</groupId>
        <artifactId>jcstress-core</artifactId>
        <version>${jcstress.version}</version>
    </dependency>
</dependencies>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <jcstress.version>0.5</jcstress.version>
    <javac.target>1.8</javac.target>
    <uberjar.name>jcstress</uberjar.name>
</properties>

代码

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering {

    int num = 0;

    boolean ready = false;

    // 线程1执行的代码
    @Actor
    public void actor1(I_Result r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    // 线程2执行的代码
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,在多线程情况下可能出现几种结果?

  • 情况1:线程1先执行actor1,这时ready = false,所以进入else分支结果为1

  • 情况2:线程2执行到actor2,执行了num = 2;和ready = true,线程1执行,这回进入 if 分支,结果为4。

  • 情况3:线程2先执行actor2,只执行num = 2;但没来得及执行 ready = true,线程1执行,还是进入else分支,结果为1。

  • 还有一种结果0。

运行测试:

mvn clean install
java -jar target/jcstress.jar

image-20200217121643544

2.3.2 指令重排

指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量的测试才能复现。

程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。

2.3.3 为什么要重排序

为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。

2.3.4 as-if-serial语义

as-if-serial 语义的意思是:不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的。

以下数据有依赖关系,不能重排序

写后读:

int a = 1;
int b = a;

写后写:

int a = 1;
int a = 2;

读后写:

int a = 1;
int b = a;
int a = 2;

编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

int a = 1;
int b = 2;
int c = a + b;

上面 3个操作的数据依赖关系如图所示:

image-20200215164952808

如上图所示a和c之间存在数据依赖关系,同时b和c之间也存在数据依赖关系。因此在最终执行的指令序列中,c不能被重排序到a和b的前面。但a和b之间没有数据依赖关系,编译器和处理器可以重排序a和b之间的执行顺序。下图是该程序的两种执行顺序。

image-20200215165003088

//可以这样:
int a = 1;
int b = 2;
int c = a + b;
//也可以重排序这样:
int b = 2;
int a = 1;
int c = a + b;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性,例如著名的 double-checked locking 模式实现单例

public final class Singleton {

    private Singleton() {}

    private static Singleton INSTANCE = null;

    public static Singleton getInstance() {
        // 实例没创建,才会进入内部的 synchronized代码块
        if (INSTANCE == null) {
            synchronized (Singleton.class) { // 也许有其它线程已经创建实例,所以再判断一次
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

synchronized 块里的非原子操作依旧可能发生指令重排

以上的实现特点是:

  • 懒惰实例化 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 但在多线程环境下,上面的代码是有问题的, INSTANCE = new Singleton() 对应的字节码为:
0: new           #3                  // class vip/ruoyun/Singleton
3: dup
4: invokespecial #4                  // Method "<init>":()V
7: putstatic     #2                  // Field INSTANCE:Lvip/ruoyun/Singleton;

其中4和7两步的顺序不是固定的,也许 jvm 会优化为:先将引用地址赋值给 INSTANCE 变量后,再执行构造方法,如果两个线程t1,t2 按如下时间序列执行:

时间1 t1 线程执行到 INSTANCE = new Singleton(); 
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0 处) 
时间3 t1 线程将引用地址赋值给 INSTANCE,这时 INSTANCE != null(7 处) 
时间4 t2 线程进入getInstance() 方法,发现 INSTANCE != null(synchronized块外),直接 返回 INSTANCE 
时间5 t1 线程执行Singleton的构造方法(4 处)

这时t1还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到的是将是一个未初始化完毕的单例。

因为 new 这个指令是非原子操作,底层是分成几条指令来执行的,加上 volatile 是禁止指令重排,保证别的线程读到的时候一定是状态和引用正常的、一个完整的对象,防止其他线程看到的是类似引用有了,内存资源却还没分配的对象。

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效。

2.3.5 volatile 保证有序性

volatile 修饰的变量,可以禁用指令重排。

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering {

    int num = 0;

    volatile boolean ready = false;

    // 线程1执行的代码
    @Actor
    public void actor1(I_Result r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    // 线程2执行的代码
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

结果:

image-20200217171445061

2.3.6 使用 synchronized保证有序性

在代码中加上synchronized关键字, synchronized 块里的非原子操作依旧可能发生指令重排

synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering {

    int num = 0;

    boolean ready = false;

    // 线程1执行的代码
    @Actor
    public void actor1(I_Result r) {
        synchronized (this) {
            if (ready) {
                r.r1 = num + num;
            } else {
                r.r1 = 1;
            }
        }
    }

    // 线程2执行的代码
    @Actor
    public void actor2(I_Result r) {
        synchronized (this) {
            num = 2;
            ready = true;
        }
    }
}

image-20200217171742510

2.3.7 happens-before规则

happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,

除了以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

规则一:线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见

public class Test01 {
    static int x;
    static Object m = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (m) {
                x = 10;
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (m) {
                System.out.println(x);
            }
        }, "t2").start();
    }
}

规则二:线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

public class Test01 {
    volatile static int x;

    public static void main(String[] args) {
        new Thread(() -> {
            x = 10;
        }, "t1").start();

        new Thread(() -> {
            System.out.println(x);
        }, "t2").start();
    }
}

规则三:线程 start 前对变量的写,对该线程开始后对该变量的读可见

public class Test03 {

    static int x;

    public static void main(String[] args) {
        x = 10;
        new Thread(() -> {
            System.out.println(x);
        }, "t2").start();
    }
}

规则四:线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或t1.join()等待它结束)

public class Test04 {

    static int x;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            x = 10;
        }, "t1");
        t1.start();

        t1.join();
        System.out.println(x);
    }
}

规则五:线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)

public class Test05 {

    static int x;

    public static void main(String[] args) {
        Thread t2 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        }, "t2");
        t2.start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            x = 10;
            t2.interrupt();
        }, "t1").start();

        while (!t2.isInterrupted()) {
            Thread.yield();
        }
        System.out.println(x);
    }
}

规则六:对变量默认值(0,false,null)的写,对其它线程对该变量的读可见。

规则七:具有传递性,如果 x happens-before y 并且 y happens-before z,那么有 x happens-before z。

2.3.8 小结

synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码。

3. CAS与原子类

3.1 CAS

CAS的全称是: Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。

CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。

CAS 它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行 +1 操作:

// 需要不断尝试
while (true) {
    int 旧值 = 共享变量; // 比如拿到了当前值 0
    int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
    /**
     *这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
     *compareAndSwap 返回 false,重新尝试,直到:
     *compareAndSwap 返回 true,表示本线程做修改的同时,别的线程没有干扰
     */
    if (compareAndSwap(旧值, 结果)) {
        // 成功,退出循环
    }
}

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

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

image-20200217210759715

AtomicInteger是CAS和volatile实现无锁并发的官方实现。

public class Demo01 {
  public static void main(String[] args) throws InterruptedException {
    AtomicInteger atomicInteger = new AtomicInteger();
    Runnable mr = () -> {
      for (int i = 0; i < 1000; i++) {
        atomicInteger.incrementAndGet();
     }
   };
    ArrayList<Thread> ts = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
      Thread t = new Thread(mr);
      t.start();
      ts.add(t);
   }
    for (Thread t : ts) {
      t.join();
   }
    System.out.println("number = " + atomicInteger.get());
 }
}

3.2 底层原理

AtomicInteger 是系统提供的 cas 原子操作类,源码如下:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    //...
}

通过源码我们可以看到,Unsafe类提供了原子操作。

3.2.1 Unsafe类介绍

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。

image-20200218114008043

通过反射的方式,结合Unsafe和volatile来实现一个AtomicInteger。

import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class TestCAS {

    public static void main(String[] args) throws InterruptedException {
        DataContainer dc = new DataContainer();
        int count = 5;

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < count; i++) {
                dc.increase();
            }
        });
        t1.start();
        t1.join();
        System.out.println(dc.getData());
    }

}

class DataContainer {

    private volatile int data;//用 volatile 来保证数据的可见性

    static final Unsafe unsafe;

    static final long DATA_OFFSET;

    static {
        try { // Unsafe 对象不能直接调用,只能通过反射获得
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
        try { // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
            DATA_OFFSET = unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }

    public void increase() {

        int oldValue;
        while (true) { // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
            oldValue = data; // cas 尝试修改 data 为 旧值 + 1,如果期间旧值被别的线程改了,返回 false
            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue + 1)) {
                return;
            }
        }
    }

    public void decrease() {
        int oldValue;
        while (true) {
            oldValue = data;
            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - 1)) {
                return;
            }
        }
    }

    public int getData() {
        return data;
    }
}
3.2.2 Unsafe 实现CAS

Unsafe 底层通过偏移量可以直接拿到存储变量的位置,就可以直接获取值。

image-20200218142314500

3.3 乐观锁和悲观锁

悲观锁从悲观的角度出发:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞。因此synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock也是一种悲观锁。性能较差!

乐观锁从乐观的角度出发:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如何没有人修改则更新,如果有人修改则重试。

CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

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

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

3.4 小结

CAS的作用? Compare And Swap,CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理器保证。

CAS的原理?CAS需要3个值:内存地址V,旧的预期值A,要修改的新值B,如果内存地址V和旧的预期值A相等就修改内存地址值为B。

4. synchronized的特性

4.1 可重入特性

4.1.1 什么是可重入

一个线程可以多次执行synchronized,重复获取同一把锁

public class Test07 {
    public static void main(String[] args) {
        Runnable sellTicket = new Runnable() {
            @Override
            public void run() {
                synchronized (Test07.class) {
                    System.out.println("我是run");
                    test01();
                }
            }
            public void test01() {
                synchronized (Test07.class) {
                    System.out.println("我是test01");
                }
            }
        };
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
    }
}
4.1.2 可重入原理

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.

4.1.3 可重入的好处
  1. 可以避免死锁
  2. 可以让我们更好的来封装代码
4.1.4 小结

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

4.2 不可中断特性

4.2.1 什么是不可中断

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

4.2.2 synchronized不可中断

synchronized 是不可中断,处于阻塞状态的线程会一直等待锁。

/**
 * 目标:演示synchronized不可中断
 * 1.定义一个Runnable
 * 2.在Runnable定义同步代码块
 * 3.先开启一个线程来执行同步代码块,保证不退出同步代码块
 * 4.后开启一个线程来执行同步代码块(阻塞状态)
 * 5.停止第二个线程
 */
public class Demo02_Uninterruptible {

    private static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 1.定义一个Runnable
        Runnable run = () -> {
            // 2.在Runnable定义同步代码块
            synchronized (obj) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "进入同步代码块");
                // 保证不退出同步代码块
                try {
                    Thread.sleep(888888);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 3.先开启一个线程来执行同步代码块
        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        // 4.后开启一个线程来执行同步代码块(阻塞状态)
        Thread t2 = new Thread(run);
        t2.start();

        // 5.停止第二个线程
        System.out.println("停止线程前");
        t2.interrupt();
        System.out.println("停止线程后");

        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}
4.2.3 ReentrantLock可中断演示

lock.lock()不可中断

lock.tryLock()可中断

/**
 * 目标:演示Lock不可中断和可中断
 */
public class Demo03_Interruptible {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        // test01();
        test02();
    }

    // 演示Lock可中断
    public static void test02() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();
            boolean b = false;
            try {
                b = lock.tryLock(3, TimeUnit.SECONDS);
                if (b) {
                    System.out.println(name + "获得锁,进入锁执行");
                    Thread.sleep(88888);
                } else {
                    System.out.println(name + "在指定时间没有得到锁做其他操作");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (b) {
                    lock.unlock();
                    System.out.println(name + "释放锁");
                }
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(run);
        t2.start();

        // System.out.println("停止t2线程前");
        // t2.interrupt();
        // System.out.println("停止t2线程后");
        //
        // Thread.sleep(1000);
        // System.out.println(t1.getState());
        // System.out.println(t2.getState());
    }

    // 演示Lock不可中断
    public static void test01() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();
            try {
                lock.lock();
                System.out.println(name + "获得锁,进入锁执行");
                Thread.sleep(88888);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(name + "释放锁");
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(run);
        t2.start();

        System.out.println("停止t2线程前");
        t2.interrupt();
        System.out.println("停止t2线程后");

        Thread.sleep(1000);
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}
4.2.4 小结

不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或等待,不可被中断。

synchronized属于不可被中断

Lock的lock方法是不可中断的

Lock的tryLock方法是可中断的

5. synchronized原理

5.1 原理

5.1.1 反汇编

通过javap反汇编学习synchronized的原理

我们编写一个简单的synchronized代码,如下:

public class Demo01 {
    private static Object obj = new Object();
    public static void main(String[] args) {
        synchronized (obj) {
            System.out.println("1");
        }
    }
    public synchronized void test() {
        System.out.println("a");
    }
}

我们要看 synchronized的原理,但是synchronized是一个关键字,看不到源码。我们可以将class文件进行反汇编。

JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。

在DOS命令行输入:

javac Demo01.java
javap -p -v -c Demo01

反汇编后的效果如下:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field obj:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #4                  // String 1
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 14
        line 11: 24
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String a
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8

可以发现synchronized关键字被编译成字节码后会被翻译成monitorenter 和monitorexit两条指令分别在同步块逻辑代码的起始位置与结束位置。

20307219-62d211cb199c1b77

5.1.2 monitorenter

首先我们来看一下JVM规范中对于monitorenter的描述:

https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

翻译过来: 每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

小结:

synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待。

5.1.3 monitorexit

首先我们来看一下JVM规范中对于monitorexit的描述:

https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻译过来:

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

小结:

monitorexit释放锁

monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

5.1.4 同步方法

synchronized是加在方法上的,锁住的整个方法。

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

可以看到同步方法在反汇编后,会增加 ACC_SYNCHRONIZED 修饰。会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。

  public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String a
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8
5.1.5 小结

通过javap反汇编我们看到synchronized使用编程了monitorentor和monitorexit两个指令.每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数,当执行monitorexit时,recursions会-1,当计数器减到0时这个线程就会释放锁.

5.1.6 面试题synchroznied出现异常会释放锁吗?

会释放锁,我们通过下面的源码进行分析

         5: monitorenter
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #4                  // String 1
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any

我们通过Exception table可以查看异常,从 6 行开始到 16 行如果有任何异常,那么就会跳到 19 行开始执行代码,可以发现 21 行有monitorexit 方法,说明如果执行异常的代码的话也是可以释放锁的。

5.1.5 面试题:synchronized与Lock的区别
  1. synchronized是关键字,而Lock是一个接口。
  2. synchronized会自动释放锁,而Lock必须手动释放锁。
  3. synchronized是不可中断的,Lock可以中断也可以不中断。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  5. synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  6. Lock可以使用读锁提高多线程读效率。ReentrantReadWriteLock读写锁,可多读。
  7. synchronized是非公平锁,ReentrantLock(独占锁)可以控制是否是公平锁(先来后到)。

5.2深入 JVM 源码

5.2.1 JVM源码下载

http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip

5.2.2 源码目录介绍
5.2.3 monitor监视器锁

可以看出无论是synchronized代码块还是synchronized方法,其线程安全的语义实现最终依赖一个叫monitor的东西,那么这个神秘的东西是什么呢?下面让我们来详细介绍一下。

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。

ObjectMonitor主要数据结构如下(只看重点代码):

ObjectMonitor() {
    //...
    _recursions   = 0;    // 线程的重入次数
    _object       = NULL; // 传入的锁对象,存储该monitor的对象
    _owner        = NULL; // 标识拥有该monitor的线程
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _cxq          = NULL ; // 多线程竞争锁时的单向列表
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    //...
}
  1. _owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
  2. _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
  3. _EntryList:cxq队列中有资格成为候选资源的线程会被移动到该队列中。
  4. _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

每一个Java对象都可以与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。

我们的Java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:monitor并不是随着对象创建而创建的。我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。每个线程都存在两个ObjectMonitor对象列表,分别为free和used列表。同时JVM中也维护着global locklist。当线程需要ObjectMonitor对象时,首先从线程自身的free表中申请,若存在则使用,若不存在则从global list中申请。

ObjectMonitor的数据结构中包含:_owner_WaitSet_EntryList ,它们之间的关系转换可以用下图表示:

image-20200218115741671

5.2.4 monitor竞争

1.执行monitorenter时,会调用InterpreterRuntime.cpp(位于:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函数。具体代码可参见HotSpot源码。

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {//偏向锁
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
IRT_END

2.对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter()

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }

#if 0
  // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif

  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

3.最终调用 ObjectMonitor::enter(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下:

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
  // 通过CAS操作尝试把monitor的_owner字段设置为当前线程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
     return ;
  }
  // 线程重入,recursions++
  if (cur == Self) {
     _recursions ++ ;
     return ;
  }
  // 如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
  if (Self->is_lock_owned ((address)cur)) {
    _recursions = 1 ;
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }

  Self->_Stalled = intptr_t(this) ;

  if (Knob_SpinEarly && TrySpin (Self) > 0) {
     Self->_Stalled = 0 ;
     return ;
  }

  JavaThread * jt = (JavaThread *) Self ;
  Atomic::inc_ptr(&_count);

  EventJavaMonitorEnter event;

  { // Change java thread status to indicate blocked on monitor enter.
    JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);

    DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
    if (JvmtiExport::should_post_monitor_contended_enter()) {
      JvmtiExport::post_monitor_contended_enter(jt, this);
    }

    OSThreadContendState osts(Self->osthread());
    ThreadBlockInVM tbivm(jt);

    Self->set_current_pending_monitor(this);

    // TODO-FIXME: change the following for(;;) loop to straight-line code.
    for (;;) {
      jt->set_suspend_equivalent();
      // 如果获取锁失败,则等待锁的释放
      EnterI (THREAD) ;
      if (!ExitSuspendEquivalent(jt)) break ;

          _recursions = 0 ;//重入清零
      _succ = NULL ;
      exit (false, Self) ;

      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);
  }

  Atomic::dec_ptr(&_count);
  Self->_Stalled = 0 ;

  // Must either set _recursions = 0 or ASSERT _recursions == 0.
  assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;

  DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);
  if (JvmtiExport::should_post_monitor_contended_entered()) {
    JvmtiExport::post_monitor_contended_entered(jt, this);
  }

  if (event.should_commit()) {
    event.set_klass(((oop)this->object())->klass());
    event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid);
    event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr()));
    event.commit();
  }

  if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
     ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
  }
}

此处省略锁的自旋优化等操作,统一放在后面 synchronzied优化中说。

以上代码的具体流程概括如下:

  1. 通过CAS尝试把monitor的owner字段设置为当前线程。
  2. 如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions ++ ,记录重入的次数。
  3. 如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
  4. 如果获取锁失败,则等待锁的释放。
5.2.5 monitor等待

竞争失败等待调用的是ObjectMonitor对象的EnterI方法(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下所示:

void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
    // Try the lock - TATAS
    if (TryLock (Self) > 0) {
        return ;
    }
    //初始化
    DeferredInitialize () ;
    if (TrySpin (Self) > 0) {
        return ;
    }
    // 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ;
    ObjectWaiter node(Self) ;
    Self->_ParkEvent->reset() ;
    node._prev   = (ObjectWaiter *) 0xBAD ;
    node.TState  = ObjectWaiter::TS_CXQ ;

    // 通过CAS把node节点push到_cxq列表中
    ObjectWaiter * nxt ;
    for (;;) {
        node._next = nxt = _cxq ;
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
        if (TryLock (Self) > 0) {
            return ;
        }
    }

    if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
        Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
    }

    TEVENT (Inflated enter - Contention) ;
    int nWakeups = 0 ;
    int RecheckInterval = 1 ;

    for (;;) {
        // 线程在被挂起前做一下挣扎,看能不能获取到锁
        if (TryLock (Self) > 0) break ;
        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }

        // 锁住自己
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            //通过互斥锁 pthread_mutex_lock 来加锁
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            // Increase the RecheckInterval, but clamp the value.
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            // 通过park将当前线程挂起,等待被唤醒
            Self->_ParkEvent->park() ;
        }

        if (TryLock(Self) > 0) break ;

        TEVENT (Inflated enter - Futile wakeup) ;
        if (ObjectMonitor::_sync_FutileWakeups != NULL) {
           ObjectMonitor::_sync_FutileWakeups->inc() ;
        }
        ++ nWakeups ;

        if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;

        if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) {
           Self->_ParkEvent->reset() ;
           OrderAccess::fence() ;
        }
        if (_succ == Self) _succ = NULL ;

        // Invariant: after clearing _succ a thread *must* retry _owner before parking.
        OrderAccess::fence() ;
    }

    UnlinkAfterAcquire (Self, &node) ;
    if (_succ == Self) _succ = NULL ;

    if (_Responsible == Self) {
        _Responsible = NULL ;
        OrderAccess::fence(); // Dekker pivot-point
    }

    if (SyncFlags & 8) {
       OrderAccess::fence() ;
    }
    return ;
}

当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁,TryLock方法实现如下:

int ObjectMonitor::TryLock (Thread * Self) {
   for (;;) {
      void * own = _owner ;
      if (own != NULL) return 0 ;
      if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
         return 1 ;
      }
      // 锁暂时是免费的,但我们输给了锁比赛。干扰-CAS失败,我们可以返回-1或重试.Retry没有太大意义,因为只是获得了锁。
      if (true) return -1 ;
   }
}

以上代码的具体流程概括如下:

  1. 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
  2. 在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
  3. node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒。
  4. 当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。
5.2.6 monitor释放

当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor的exit方法中。

(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下所示:

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ;
   if (THREAD != _owner) {
     if (THREAD->is_lock_owned((address) _owner)) {
       assert (_recursions == 0, "invariant") ;
       _owner = THREAD ;
       _recursions = 0 ;
       OwnerIsThread = 1 ;
     } else {
       assert(false, "Non-balanced monitor enter/exit!");
       if (false) {
          THROW(vmSymbols::java_lang_IllegalMonitorStateException());
       }
       return;
     }
   }

   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }
   if ((SyncFlags & 4) == 0) {
      _Responsible = NULL ;
   }

   for (;;) {
      if (Knob_ExitPolicy == 0) {
         OrderAccess::release_store_ptr (&_owner, NULL) ;   // drop the lock
         OrderAccess::storeload() ;                         // See if we need to wake a successor
         if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
            return ;
         }
         if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
            return ;
         }
      } else {
         if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
            OrderAccess::release_store_ptr (&_owner, NULL) ;   // drop the lock
            OrderAccess::storeload() ;
            // Ratify the previously observed values.
            if (_cxq == NULL || _succ != NULL) {
                TEVENT (Inflated exit - simple egress) ;
                return ;
            }
            if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
               TEVENT (Inflated exit - reacquired succeeded) ;
               return ;
            }
            TEVENT (Inflated exit - reacquired failed) ;
         } else {
            TEVENT (Inflated exit - complex egress) ;
         }
      }

      guarantee (_owner == THREAD, "invariant") ;

      ObjectWaiter * w = NULL ;
      int QMode = Knob_QMode ;//EntryList-CXQ策略-队列规则,默认 0
      // qmode = 2:直接绕过EntryList队列,从cxq队列中获取线程用于竞争锁
      if (QMode == 2 && _cxq != NULL) {
          w = _cxq ;
          assert (w != NULL, "invariant") ;
          assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
          ExitEpilog (Self, w) ;//释放锁,唤醒线程
          return ;
      }
      // qmode =3:cxq队列插入EntryList尾部
      if (QMode == 3 && _cxq != NULL) {
          w = _cxq ;
          for (;;) {
             ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
             if (u == w) break ;
             w = u ;
          }
          ObjectWaiter * q = NULL ;
          ObjectWaiter * p ;
          for (p = w ; p != NULL ; p = p->_next) {
              guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
              p->TState = ObjectWaiter::TS_ENTER ;
              p->_prev = q ;
              q = p ;
          }
          ObjectWaiter * Tail ;
          for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ;
          if (Tail == NULL) {
              _EntryList = w ;
          } else {
              Tail->_next = w ;
              w->_prev = Tail ;
          }
      }
      // qmode =4:cxq队列插入到_EntryList头部
      if (QMode == 4 && _cxq != NULL) {
          w = _cxq ;
          for (;;) {
             assert (w != NULL, "Invariant") ;
             ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
             if (u == w) break ;
             w = u ;
          }
          ObjectWaiter * q = NULL ;
          ObjectWaiter * p ;
          for (p = w ; p != NULL ; p = p->_next) {
              guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
              p->TState = ObjectWaiter::TS_ENTER ;
              p->_prev = q ;
              q = p ;
          }

          if (_EntryList != NULL) {
              q->_next = _EntryList ;
              _EntryList->_prev = q ;
          }
          _EntryList = w ;
      }

      w = _EntryList  ;
      if (w != NULL) {
          ExitEpilog (Self, w) ;
          return ;
      }

      w = _cxq ;
      if (w == NULL) continue ;

      for (;;) {
          assert (w != NULL, "Invariant") ;
          ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
          if (u == w) break ;
          w = u ;
      }
      TEVENT (Inflated exit - drain cxq into EntryList) ;
      //将QMode == 1 :cxq排放到EntryList后面,然后反转链表
      if (QMode == 1) {
         ObjectWaiter * s = NULL ;
         ObjectWaiter * t = w ;
         ObjectWaiter * u = NULL ;
         while (t != NULL) {
             guarantee (t->TState == ObjectWaiter::TS_CXQ, "invariant") ;
             t->TState = ObjectWaiter::TS_ENTER ;
             u = t->_next ;
             t->_prev = u ;
             t->_next = s ;
             s = t;
             t = u ;
         }
         _EntryList  = s ;
         assert (s != NULL, "invariant") ;
      } else {
         // QMode == 0 or QMode == 2
         _EntryList = w ;
         ObjectWaiter * q = NULL ;
         ObjectWaiter * p ;
         for (p = w ; p != NULL ; p = p->_next) {
             guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
             p->TState = ObjectWaiter::TS_ENTER ;
             p->_prev = q ;
             q = p ;
         }
      }
      if (_succ != NULL) continue;

      w = _EntryList  ;
      if (w != NULL) {
          guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
          ExitEpilog (Self, w) ;//释放锁,唤醒线程
          return ;
      }
   }
}

退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。

根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成,实现如下:

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
   //退出协议:
   // 1. ST _succ = wakee 
   // 2. membar #loadstore | #storestore;
   // 2. ST _owner = NULL 置空
   // 3. unpark(wakee)执行解锁方法

   _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
   ParkEvent * Trigger = Wakee->_event ;
   Wakee  = NULL ;

   // Drop the lock
   OrderAccess::release_store_ptr (&_owner, NULL) ;
   OrderAccess::fence() ;                               // ST _owner vs LD in unpark()

   DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
   Trigger->unpark() ;//唤醒之前被pack()挂起的线程

   // Maintain stats and report events to JVMTI
   if (ObjectMonitor::_sync_Parks != NULL) {
      ObjectMonitor::_sync_Parks->inc() ;
   }
}

被唤醒的线程,会回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,继续执行monitor的竞争。

// park self
if (_Responsible == Self || (SyncFlags & 1)) {
   TEVENT (Inflated enter - park TIMED) ;
   Self->_ParkEvent->park ((jlong) RecheckInterval) ;//唤醒然后执行下面的代码
   RecheckInterval *= 8 ;
   if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
   Self->_ParkEvent->park() ;//唤醒然后执行下面的代码
}

整个过程如下图所示

image-20200218115741671

5.2.7 monitor是重量级锁

可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。

通过下面的源码,可以发现底层是用的 pthread_cond(条件变量) 和 pthread_mutex(互斥锁)来进行加锁解锁操作。

void os::PlatformEvent::park() {       // AKA "down()"
  int v ;
  for (;;) {
      v = _Event ;
      if (Atomic::cmpxchg (v-1, &_Event, v) == v) break ;
  }
  if (v == 0) {
     int status = pthread_mutex_lock(_mutex);
     ++ _nParked ;
     while (_Event < 0) {
        status = pthread_cond_wait(_cond, _mutex);
        if (status == ETIME) { status = EINTR; }
        assert_status(status == 0 || status == EINTR, status, "cond_wait");
     }
     -- _nParked ;

    _Event = 0 ;
    status = pthread_mutex_unlock(_mutex);
    OrderAccess::fence();
  }
}

用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:

Linux系统体系结构.PNG

从上图可以看出, Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。

  • 内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。

  • 用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。

  • 系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执行某些操作时,例如 I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内核态)。 系统调用的过程可以简单理解为:

  1. 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈, 以此表明需要操作系统提供的服务。
  2. 用户态程序执行系统调用。
  3. CPU切换到内核态,并跳到位于内存指定位置的指令。
  4. 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
  5. 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。

由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率低的原因。

6. JDK6 synchronized优化

6.1 synchronized 锁升级过程

高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

“轻量级锁 CAS 操作”的图片搜索结果

6.2 Java 对象的布局

术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

image-20200216223811744

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

6.2.1 对象头

当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?

是存在锁对象的对象头中的。

HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc的定义对应 arrayOop.hpp 。

class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }

  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

从 instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot源码中的 oop.hpp 文件中。

class oopDesc {
  friend class VMStructs;//友元类
 private:
  volatile markOop  _mark;
  union _metadata {//对象头,联合体
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  static BarrierSet* _bs;
  //...
};

在普通实例对象中, oopDesc的定义包含两个成员,分别是 _mark 和 _metadata。

  • _mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有关的信息

  • _metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针_compressed_klass 表示压缩类指针。

对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针(一般为c++实现)。

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类型是 markOop 。源码位于 markOop.hpp 中。

锁的状态描述

//    [JavaThread* | epoch | age | 1 | 01]       线程中的锁偏锁状态
//    [0           | epoch | age | 1 | 01]       锁匿名偏向状态

锁三种状态:锁定、解锁和监视。

//    [ptr             | 00]  locked             轻量级加锁
//    [header      | 0 | 01]  unlocked           无锁
//    [ptr             | 10]  monitor            重量级锁,monitor进入,锁膨胀
//    [ptr             | 11]  marked             用于标记对象,用来 GC

在 64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:

//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

image-20200218133044362

image-20200218133134918

在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

“轻量级锁 CAS 操作”的图片搜索结果

klass pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。 如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。

为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  1. 每个Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)

在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;

在 64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;

6.2.2 实例数据

就是类中定义的成员变量。

6.2.3 对齐填充

对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

6.2.4 查看Java对象布局
<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.9</version>
</dependency>
public class LockObj{
    private int x;//4 个字节
}

LockObj obj = new LockObj();
system.out.print(ClassLayout.parseInstance(obj).toprintable);

开启指针压缩 - XX:+UseCompressedOops ,占有 16 个字节

image-20200218134930651

关闭指针压缩 - XX:-UseCompressedOops ,占有 24 个字节

image-20200218134855679

Hashcode并不是开始就存在的,因为产生 hashcode 需要一定的耗时,所以一般都是在执行hashCode() 方法的时,才会保存到对象头中。下图中是有 hashCode 的,可以和上图进行比较。

image-20200218135411192

6.2.5 小结

Java对象由3部分组成,对象头,实例数据,对齐数据

对象头分成两部分:Mark World + Klass pointer

6.3 偏向锁

6.3.1 什么是偏向锁

偏向锁是JDK 6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

image-20200218133134918

不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

6.3.2 偏向锁原理

当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

  1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
  2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高

持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

6.3.3 偏向锁的撤销
  1. 偏向锁的撤销动作必须等待全局安全点(全局安全点:所有线程都需要停止)
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

偏向锁在 Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 -XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 -XX: -UseBiasedLocking=false 参数关闭偏向锁。

6.3.4 偏向锁好处

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。但在应用程序启动几秒钟之后才激活,可以使用 - XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 XX: -UseBiasedLocking=false 参数关闭偏向锁。

6.3.5 小结

偏向锁的原理是什么?

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

偏向锁的好处是什么?

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

偏向多的坏处是什么?

如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的,偏向锁的撤销动作必须等待全局安全点,也就是说所有线程都需要停止才可以撤销偏向锁,所以这个动作是非常耗时的。

6.4 轻量级锁

6.4.1 什么是轻量级锁

轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。

引入轻量级锁的目的:

在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

6.4.1 简单理解

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。这就好比:

学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没有竞争,继续上他的课。

如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程 A 随即升级为重量级锁,进入重量级锁的流程。

而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来。

假设有两个方法同步块,利用同一个对象加锁。

6.4.2 轻量级锁原理

当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

  1. 判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Reocrd中的owner指向当前对象。
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
  3. 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

image-20200218140514253

“轻量级锁 CAS 操作”的图片搜索结果

“轻量级锁 CAS 操作”的图片搜索结果

6.4.3 轻量级锁的释放

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据。
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功。
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

6.4.4 轻量级锁好处

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗

6.4.5 小结

轻量级锁的原理是什么?

将对象的Mark Word复制到栈帧中的Lock Recod中。Mark Word更新为指向Lock Record的指针。

轻量级锁好处是什么?

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗

6.5 自旋锁

6.5.1 自旋锁原理
synchronized (Demo01.class) {
 ...
  System.out.println("aaa");
}

前面我们讨论 monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋) , 这项技术就是所谓的自旋锁。

自旋锁在JDK 1.4.2中就已经引入 ,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 6中 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性 能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数-XX : PreBlockSpin来更改。

6.5.2 适应性自旋锁

在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。

6.6 锁消除

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。

public class Demo01 {
    public static void main(String[] args) {
        contactString("aa", "bb", "cc");
    }
    public static String contactString(String s1, String s2, String s3) {
        return new StringBuffer().append(s1).append(s2).append(s3).toString();
    }
}

StringBuffer的append ( ) 是一个同步方法,锁就是this也就是(new StringBuilder())。虚拟机发现它的动态作用域被限制在concatString( )方法内部。也就是说, new StringBuilder()对象的引用永远不会“逃逸”到concatString ( )方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

6.7 锁粗化

什么是锁粗化?

JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。

public class Demo01 {
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            sb.append("aa");
        }
        System.out.println(sb.toString());
    }
}

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

7.如何编写高性能代码

7.1减少synchronized的范围

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

synchronized (Demo01.class) {
    System.out.println("aaa");
}

7.2 降低 synchronized 锁的粒度

不要用同一把锁,锁住业务不相关的代码。

7.2.1 Hashtable
Hashtable hs = new Hashtable();
hs.put("aa", "bb");
hs.put("xx", "yy");

Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁!

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
	//...code
    public synchronized V put(K key, V value) {
        //...
    }

    public synchronized V remove(Object key) {
        //...
    }

    public synchronized void putAll(Map<? extends K, ? extends V> t) {
        //...
    }

    public synchronized void clear() {
        //...
    }
    public synchronized Object clone() {
        //...
    }
	//code...
}

相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用!

“hashtable 锁”的图片搜索结果

7.2.2 ConcurrentHashMap

ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment, 如下图(jdk1.7):

img

更多请参考:http://www.justdojava.com/2019/12/18/java-collection-15.1/

7.2.3 LinkedBlockingQueue

为了保证队列的线程的安全,如果用同一把锁锁住一个队列的put()和take(),那么同时只能实行一条指令。

image-20200218143713346

LinkedBlockingQueue 入队和出队使用不同的锁,相对于读写只有一个锁效率要高

image-20200218143651692

7.3 读写分离

读取时不加锁,写入和删除时加锁

CopyOnWriteArrayList和ConyOnWriteSet

如果你喜欢我的文章,可以关注我的掘金、公众号、博客、简书或者Github!

简书: https://www.jianshu.com/u/a2591ab8eed2

GitHub: https://github.com/bugyun

Blog: https://ruoyun.vip

掘金: https://juejin.im/user/56cbef3b816dfa0059e330a8/posts

CSDN: https://blog.csdn.net/zxloveooo

欢迎关注微信公众号

image

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值