跳到正文
返回

Java 内存模型

发表于 更新于
浏览量: --
系列文章: JVM 快速入门 4 / 4
  1. JVM 内存结构
  2. JVM 垃圾回收
  3. 字节码与类加载
  4. Java 内存模型 当前文章

封面来源:由博主个人绘制,如需使用请联系博主。

参考链接:黑马程序员JVM快速入门

本文涉及的代码:mofan212/jvm-demo

除特别注明外,本文内容都基于 JDK 1.8

1. Java 内存模型

注意「Java 内存结构」和「Java 内存模型」的区别,「Java 内存模型」指的是 Java Memory Model,即 JMM。

更权威的解释参考:JSR-133 Java Memory Model and Thread Specification 1.0 Proposed Final Draft

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

2. 原子性

2.1 自增与自减

所谓原子性,即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

通过一个例子回顾一下:有一个初始值为 0 的静态变量,一个线程对其自增,一个线程对其自减,进行 5000 次,最终结果会是 0 吗?

结果不一定是 0。

static int i = 0;

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

    Thread t2 = new Thread(() -> {
        for (int j = 0; j < 5000; j++) {
            i--;
        }
    });

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

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

    System.out.println(i);
}

2.2 问题分析

以上的结果可能是正数、负数和零,为什么呢?

因为 Java 中对静态变量的自增、自减操作并不是原子操作。

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

getstatic     i // 获取静态变量 i 的值
iconst_1        // 准备常量 1
iadd            // 加法
pubstatic     i // 将修改后的值存入静态变量 i

对应 i-- 也是类似:

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

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

主内存与工作内存之间的数据交换

如果是在单线程中,上述 8 行指令是顺序执行的,不会出现交错执行:

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

但在多线程的环境下,这 8 行指令可能会交错执行,导致最终结果错误。

出现负数的情况:

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

出现正数的情况:

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

2.3 解决方法

使用关键字 synchronized

语法:

synchronized ( 对象 ) {
    要作为原子操作的代码
}

使用 synchronized 优化先前的自增、自减代码:

static int i = 0;

static final Object obj = new Object();

public static void main(String[] args) throws InterruptedException {

    Thread t1 = new Thread(() -> {
        for (int j = 0; j < 5000; j++) {
            // 加锁、减锁操作会执行 5000 次,更建议将其放到 for 循环外
            synchronized (obj) {
                i++;
            }
        }
    });

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

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

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

    System.out.println(i);
}

如何理解呢?

可以把 obj 想象成一个房间,线程 t1t2 想象成两个人。当线程 t1 执行到 synchronized (obj) 时就好比 t1 进入了这个房间,并反手锁住了门,在门内执行 i++ 代码。这时候如果 t2 也运行到了 synchronized (obj),它发现门被锁住了,就只能在门外等待。

t1 执行完 synchronized 块内的代码后,才会解开门上的锁,从 obj 房间里出来,t2 线程这时才可以进入 obj 房间,反锁住门,执行它的 i-- 代码。

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

3. 可见性

3.1 退不出的循环

在下列代码中,main 线程对 run 变量的修改对于 t 线程不可见,导致 t 线程无法停止:

static boolean run =  true;

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

    t.start();

    Thread.sleep(1000);

    // 线程 t 并不按预想的那样停下来
    run = false;
}

这是为什么呢?

初始状态下,t 线程刚开始从主内存中读取了 run 的值到工作内存中:

t线程读取主内存中的run变量值

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

JIT缓存run变量的值

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

main线程修改run的值

3.2 解决方法

使用 volatile (易变的)关键字。

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

static volatile boolean run =  true;

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

    t.start();

    Thread.sleep(1000);

    // 线程 t 并不按预想的那样停下来
    run = false;
}

3.3 可见性

先前例子体现的就是可见性,它保证在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见,不能保证原子性, 仅用在一个写线程,多个读线程的情况。

从字节码角度可以这样理解:

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

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

如果不使用 volatile 关键字,而是在死循环内部加入 System.out.println() 方法,会发现线程 t 也能正常停止,这是为什么呢?

// PrintStream
public void println(boolean x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

这是因为在 println() 方法内部使用了 synchronized 关键字,保证了主内存和工作内存之间的数据一致。

4. 有序性

4.1 诡异的结果

int num = 0;
boolean ready = false;

// 线程 1 执行此方法
public void actor1(I_Result r) {
    if (ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}

// 线程 2 执行此方法
public void actor2(I_Result r) {
    num = 2;
    ready = true;
}

I_Result 是一个对象,内部有一个 r1 属性来保存结果,请问 r1 有几种可能?

  1. 线程 1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  2. 线程 2 先执行 num = 2,但没来得及执行 ready = true,线程 1 执行,还是进入 else 分支,结果为 1
  3. 线程 2 执行到 ready = true,线程 1 执行,进入 if 分支,结果为 4(因为在线程 2 执行到 ready = true 时,把 num = 2 执行了)

实际情况并不只是上述 3 种情况,结果还可能是 0:

WTF?怎么会这样?😵‍💫

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

可以借助 OpenJDK: jcstress 来复现。

新建 Maven 项目,添加 jcstress 依赖:

<dependency>
    <groupId>org.openjdk.jcstress</groupId>
    <artifactId>jcstress-core</artifactId>
    <version>0.16</version>
</dependency>

提供以下测试类:

@State
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!")
public class ConcurrencyTest {
    int num = 0;
    boolean ready = false;

    public ConcurrencyTest() {
    }

    @Actor
    public void actor1(I_Result r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

执行下列 Maven 命令,对项目进行打包:

mvn clean package

配置 ConcurrencyTest 的运行方式:

IDEA内配置ConcurrencyTest的运行方式

运行后会有类似以下结果:

RESULT      SAMPLES     FREQ       EXPECT  DESCRIPTION
       0       43,219    0.01%  Interesting  !!!
       1  128,331,985   43.27%   Acceptable  ok
       4  168,193,647   56.71%   Acceptable  ok

可以看到,确实存在结果为 0 的情况,只不过占比很少,只有 0.01%

4.2 解决方法

使用 volatile 关键词对 ready 成员变量进行修饰:

volatile boolean ready = false;

也就是说,volatile 不仅可以保证可见性,还可以避免指令重排,但记住不能保证原子性。

用相同的方式再次运行 ConcurrencyTest,结果出现:

Interesting tests: No matches.

也就是没有出现结果为 0 的情况。

4.3 理解有序性

同一个线程内,JVM 会在不影响正确性的前提下调整语句的执行顺序:

static int i;
static int j;

// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时的操作
j = ...;

可以看到,至于是先执行 i 还是先执行 j,对最终结果都不会产生影响。所以上述代码真正执行时的顺序可以是:

i = ...; // 较为耗时的操作
j = ...;

也可以是:

j = ...;
i = ...; // 较为耗时的操作

这种特性被称为「指令重排」,多线程下「指令重排」会影响正确性,比如著名的 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;
    }
}

上述单例模式的特点是:

但在多线程环境下,上述实现存在问题,INSTANCE = new Singleton(); 对应的字节码是:

0: new               #2                                 // class indi/mofan/Singleton
3: dup
4: invokespecial     #3                                 // Method "<init>":()V
7: putstatic         #4                                 // Field INSTANCE:Lindi/mofan/Singleton;

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

时间 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 拿到的将是一个未初始化完毕的单例对象。

解决方法也很简单,对 INSTANCE 使用 volatile 关键字修饰即可,禁用指令重排。

4.4 happens-bofore

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

注意: 上述所说的 变量 都是指静态变量或成员变量。

5. CAS 与原子类

5.1 CAS

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

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

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

CAS 底层依赖于 Unsafe 类直接调用操作系统底层的 CAS 指令,下面是直接使用 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();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < count; i++) {
                dc.decrease();
            }
        });

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

        t1.join();
        t2.join();
        System.out.println(dc.getData());
    }
}

class DataContainer {

    // data 必须被 volatile 修饰,保证其可见性
    private volatile int data;
    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 = this.data;
            /*
             * cas 尝试修改 data 为「旧值 + 1」,如果修改期间旧值被其他变量改了,则返回 false
             * 流程如下:
             * 1. 通过 this 和 DATA_OFFSET 获取到当前对象中 data 的最新值
             * 2. 使用 data 的最新值与 oldValue 进行比较
             * 3. 如果两者不等,则有其他线程干扰,返回 false,本次修改失败
             * 4. 如果两者相等,则没有其他线程干扰,执行 oldValue + 1 操作,并将结果设置给 data,最终返回 true
             */
            if (UNSAFE.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue + 1)) {
                return;
            }
        }
    }

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

    public int getData() {
        return data;
    }
}

5.2 乐观锁与悲观锁

CAS 基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量:「就算改了也没关系,我吃亏点再重试呗」。

synchronized 关键字基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量:「我上了锁你们都别想改,我改完了解开锁,你们才有机会」。

5.3 原子操作类

JUC(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如 AtomicIntegerAtomicBoolean 等,它们底层采用了 CAS + volatile 来实现的。

使用 AtomicInteger 改写先前的例子:

private static final AtomicInteger I = new AtomicInteger(0);

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

    Thread t2 = new Thread(() -> {
        for (int j = 0; j < 5000; j++) {
            // i--
            I.getAndDecrement();
        }
    });

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

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

    System.out.println(I);
}

6. synchronized 优化

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

6.1 轻量级锁

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

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

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

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

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

static Object obj = new Object();

public static void method1() {
    synchronized (obj) {
        // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized (obj) {
        // 同步块 B
    }
}

每个线程的栈帧都会包含一个锁记录结构,内部可以存储锁定对象的 Mark Word。

线程 1对象 Mark Word线程 2
访问同步块 A,把 Mark 复制到线程 1 的锁记录01(无锁)-
CAS 修改 Mark 为线程 1 锁记录地址01(无锁)-
成功(加锁)00(轻量锁)线程 1 锁记录地址-
执行同步块 A00(轻量锁)线程 1 锁记录地址-
访问同步块 B,把 Mark 复制到线程 1 的锁记录00(轻量锁)线程 1 锁记录地址-
CAS 修改 Mark 为线程 1 锁记录地址00(轻量锁)线程 1 锁记录地址-
失败(发现是自己的锁)00(轻量锁)线程 1 锁记录地址-
锁重入00(轻量锁)线程 1 锁记录地址-
执行同步块 B00(轻量锁)线程 1 锁记录地址-
同步块 B 执行完毕00(轻量锁)线程 1 锁记录地址-
同步块 A 执行完毕00(轻量锁)线程 1 锁记录地址-
成功(解锁)01(无锁)-
-01(无锁)访问同步块 A,把 Mark 复制到线程 2 的锁记录
-01(无锁)CAS 修改 Mark 为线程 2 锁记录地址
-00(轻量锁)线程 2 锁记录地址成功(加锁)
-

6.2 锁膨胀

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

static Object obj = new Object();

public static void method1() {
    synchronized (obj) {
        // 同步块
    }
}
线程 1对象 Mark Word线程 2
访问同步块,把 Mark 复制到线程 1 的锁记录01(无锁)-
CAS 修改 Mark 为线程 1 锁记录地址01(无锁)-
成功(加锁)00(轻量锁)线程 1 锁记录地址-
执行同步块00(轻量锁)线程 1 锁记录地址-
执行同步块00(轻量锁)线程 1 锁记录地址访问同步块,把 Mark 复制到线程 2 的锁记录
执行同步块00(轻量锁)线程 1 锁记录地址CAS 修改 Mark 为线程 2 锁记录地址
执行同步块00(轻量锁)线程 1 锁记录地址失败(发现其他线程已经抢占了锁)
执行同步块00(轻量锁)线程 1 锁记录地址CAS 修改 Mark 为重量级锁
执行同步块10(重量锁)重量锁指针阻塞中
执行完毕10(重量锁)重量锁指针阻塞中
失败(解锁)10(重量锁)重量锁指针阻塞中
释放重量锁,唤起阻塞线程竞争10(重量锁)阻塞中
-10(重量锁)竞争重量锁
-10(重量锁)成功(加锁)
-

6.3 重量锁

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

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

自旋重试成功的情况:

线程 1(CPU 1 上)对象 Mark Word线程 2(CPU 2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行完毕10(重量锁)重量锁指针自旋重试
成功(解锁)10(重量锁)自旋重试
-10(重量锁)重量锁指针成功(加锁)
-10(重量锁)重量锁指针执行同步块
-

自旋重试失败的情况:

线程 1(CPU 1 上)对象 Mark Word线程 2(CPU 2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针阻塞
-

6.4 偏向锁

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

可以参考这篇论文:Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing

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

static Object obj = new Object();

public static void method1() {
    synchronized (obj) {
        // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized (obj) {
        // 同步块 B
    }
}
线程 1对象 Mark
访问同步块 A,检查 Mark 中是否有线程 ID101(无锁可偏向)
尝试加偏向锁101(无锁可偏向)对象 hashCode
成功101(无锁可偏向)线程 ID
执行同步块 A101(无锁可偏向)线程 ID
访问同步块 B,检查 Mark 中是否有线程 ID101(无锁可偏向)线程 ID
是自己的线程 ID,锁是自己的,无需做更多操作101(无锁可偏向)线程 ID
执行同步块 B101(无锁可偏向)线程 ID
执行完毕101(无锁可偏向)对象 hashCode

6.5 其他优化

减少上锁时间

同步块内的代码要尽量少

减少锁的粒度

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

锁粗化

多次循环进入同步块不如同步块内多次循环。

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

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

锁消除

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

读写分离

如果这篇文章对你有帮助,可以通过
支付宝
支付宝
微信
微信
请我喝杯 Coffee ☕


上一篇
爱的艺术
下一篇
字节码与类加载