Bootstrap

多线程学习篇四:synchronized

1. synchronized 的使用

1.1 作用于实例方法

@Slf4j(topic = "c.Test01")
public class Test01 {
    public synchronized void method1() {
        // 代码逻辑
    }
}

等价于下列写法: 

@Slf4j(topic = "c.Test01")
public class Test01 {
    public void method1() {
        synchronized (this) {
            // 代码逻辑
        }
    }
}

1.2 作用于静态方法

@Slf4j(topic = "c.Test02")
public class Test02 {
    public synchronized static void method1() {
       // 代码逻辑
    }
}

等价于下列写法: 

@Slf4j(topic = "c.Test02")
public class Test02 {
    public static void method1() {
        synchronized (Test02.class) {
            // 代码逻辑
        }
    }
}

2. “线程八锁” 

2.1 案例1

@Slf4j(topic = "c.Lock01")
public class Lock01 {
    public synchronized void method1() {
        log.info("m1");
    }

    public synchronized void method2() {
        log.info("m2");
    }

    public static void main(String[] args) {
        Lock01 lock01 = new Lock01();
        new Thread(lock01::method1).start();
        new Thread(lock01::method2).start();
    }
}

输出结果:

  • case1:m1 → m2
  • case2:m2 → m1

2.2 案例2

@Slf4j(topic = "c.Lock02")
public class Lock02 {
    public synchronized void method1() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("m1");
    }

    public synchronized void method2() {
        log.info("m2");
    }

    public static void main(String[] args) {
        Lock02 lock02 = new Lock02();
        new Thread(lock02::method1).start();
        new Thread(lock02::method2).start();
    }
}

输出结果:

  • case1:1s后 → m1 → m2
  • case2:m2 → 1s后 → m1

2.3 案例3

@Slf4j(topic = "c.Lock03")
public class Lock03 {
    public synchronized void method1() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("m1");
    }

    public synchronized void method2() {
        log.info("m2");
    }

    public void method3() {
        log.info("m3");
    }

    public static void main(String[] args) {
        Lock03 lock03 = new Lock03();
        new Thread(lock03::method1).start();
        new Thread(lock03::method2).start();
        new Thread(lock03::method3).start();
    }
}

可能调用顺序:

  • method1 → method2 → method3:m3→ 1s后 → m1 → m2
  • method1 → method3 → method2:m3→ 1s后 → m1 → m2
  • method2 → method1 → method3:m2 → m3→ 1s后 → m1
  • method2 → method3 → method1:m2 → m3→ 1s后 → m1
  • method3 → method1 → method2:m3→ 1s后 → m1 → m2
  • method3 → method2 → method1:m3 → m2→ 1s后 → m1

输出结果(去重):

  • case1:m3→ 1s后 → m1 → m2
  • case2:m2 → m3→ 1s后 → m1
  • case2:m3 → m2→ 1s后 → m1

2.4 案例4

@Slf4j(topic = "c.Lock04")
public class Lock04 {
    public synchronized void method1() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("m1");
    }

    public synchronized void method2() {
        log.info("m2");
    }

    public static void main(String[] args) {
        Lock04 lock041 = new Lock04();
        Lock04 lock042 = new Lock04();

        new Thread(lock041::method1).start();
        new Thread(lock042::method2).start();
    }
}

输出结果:

  • case1:m2 → 1s后 → m1

2.5 案例5

@Slf4j(topic = "c.Lock05")
public class Lock05 {
    public synchronized void method1() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("m1");
    }

    public static synchronized void method2() {
        log.info("m2");
    }

    public static void main(String[] args) {
        Lock05 lk = new Lock05();

        new Thread(() -> lk.method1()).start();
        new Thread(() -> lk.method2()).start();
    }
}

输出结果:

  • case1:m2 → 1s后 → m1

2.6 案例6

@Slf4j(topic = "c.Lock06")
public class Lock06 {
    public static synchronized void method1() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("m1");
    }

    public static synchronized void method2() {
        log.info("m2");
    }

    public static void main(String[] args) {
        new Thread(Lock06::method1).start();
        new Thread(Lock06::method2).start();
    }
}

输出结果:

  • case1:1s后 → m1 → m2
  • case2:m2 → 1s后 → m1

2.7 案例7

@Slf4j(topic = "c.Lock07")
public class Lock07 {
    public synchronized void method1() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("m1");
    }

    public static synchronized void method2() {
        log.info("m2");
    }

    public static void main(String[] args) {
        Lock07 lk1 = new Lock07();
        Lock07 lk2 = new Lock07();

        new Thread(() -> lk1.method1()).start();
        new Thread(() -> lk2.method2()).start();
    }
}

输出结果:

  • case1:m2 → 1s后 → m1

2.8 案例8

@Slf4j(topic = "c.Lock08")
public class Lock08 {
    public static synchronized void method1() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("m1");
    }

    public static synchronized void method2() {
        log.info("m2");
    }

    public static void main(String[] args) {
        Lock08 lk1 = new Lock08();
        Lock08 lk2 = new Lock08();

        new Thread(() -> lk1.method1()).start();
        new Thread(() -> lk2.method2()).start();
    }
}

输出结果:

  • case1:1s后 → m1 → m2
  • case2:m2 → 1s后 → m1

3. Synchronized 进阶

3.1 对象头

3.1.1 32 位虚拟机
3.1.1.1 普通对象

3.1.1.2 数组对象

3.1.1.3 Mark Word

3.1.2 64 位虚拟机
3.1.2.1 普通对象

3.1.2.2 数组对象

3.1.2.3 Mark Word

3.2  Monitor原理

Monitor 被翻译为监视器管程,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

3.2.1 Monitor 结构

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj)就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行synchronized(obj),就会进入EntryList(线程进入 BLOCKED 状态)
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,可以通过 notify / notifyAll 方法将线程从 WaitSet 转移到 EntryList 竞争锁

3.3 锁升级

3.3.1 轻量级锁(不涉及 Monitor )

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。通过下方示例代码,图示加锁、解锁过程

public class Lock01 {

    private static final Object LOCK = new Object();

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

    public static void method2() {
        synchronized (LOCK) {
            // 同步块 B
        }
    }
}
3.3.1.1 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

3.3.1.2 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

3.3.1.3 CAS 分成两种情况
3.3.1.3.1 CAS 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

3.3.1.3.2 CAS 失败,有两种情况
  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

3.3.1.4 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
3.3.1.5 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
3.3.2 锁膨胀(轻量级锁膨胀为重量级锁)

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

3.3.2.1 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

3.3.2.2 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
  • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  • 然后自己进入 Monitor 的 EntryList(线程进入 BLOCKED 状态)

3.3.2.3 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
3.3.3 自旋优化(竞争重量级锁)

重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)

3.3.3.1 自选成功情况
线程1(core1上)对象 Mark线程1(core1上)
-10 (重量锁)-
访问同步块,获取 monitor10 (重量锁)重量锁指针-
成功(加锁)10 (重量锁)重量锁指针-
执行同步块10 (重量锁)重量锁指针-
执行同步块10 (重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10 (重量锁)重量锁指针自选重试
执行完毕10 (重量锁)重量锁指针自选重试
成功(解锁)00(无锁)自选重试
-10 (重量锁)重量锁指针访问同步块,获取 monitor
-10 (重量锁)重量锁指针成功(加锁)
-。。。。。。
3.3.3.2 自选失败情况
线程1(core1上)对象 Mark线程1(core1上)
-10 (重量锁)-
访问同步块,获取 monitor10 (重量锁)重量锁指针-
成功(加锁)10 (重量锁)重量锁指针-
执行同步块10 (重量锁)重量锁指针-
执行同步块10 (重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10 (重量锁)重量锁指针自选重试
执行同步块10 (重量锁)重量锁指针自选重试
执行同步块10 (重量锁)重量锁指针自选重试
执行同步块10 (重量锁)重量锁指针阻塞
3.3.3.3 小结 
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能
3.3.4 偏向锁(比轻量锁更轻的锁)

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

PS:这里的线程 id 是操作系统赋予的 id 和 Thread 的id是不同的

3.3.4.1 偏向状态

我们再回忆一下上文介绍的,64 位操作系统的 Mark 头

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

PS:VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

3.3.4.2 测试偏向锁
3.3.4.2.1 添加依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>
3.3.4.2.2 测试代码
@Slf4j(topic = "c.Lock02")
public class Lock02 {

    private static final Object OBJECT = new Object();

    public static void main(String[] args) throws Exception {
        log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
        synchronized (OBJECT) {
            log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
        }
        log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
    }
}
3.3.4.2.3 测试结果

通过结果得出以下结论:

  • Mark Word:8 * 8 = 64 bits
  • Klass Word:4 * 8 = 32 bits
  • 对其填充:4 * 8 = 32 bits
  • Object Head : Mark Word + Klass Word = 96 bits
  • 其中 Mark Word 的最后三位是 101(5),即偏向状态。处于偏向锁的对象解锁后,线程 id 仍存储于对象头中,也就是偏向某个线程了
3.3.4.3 撤销偏向 -- 调用 hashcode 方法

正常状态对象一开始是没有 hashCode 的,第一次调用才生成,调用了 hashCode() 后会撤销该对象的偏向锁,效果演示如下:

@Slf4j(topic = "c.Lock02")
public class Lock02 {

    private static final Object OBJECT = new Object();

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

        OBJECT.hashCode();
        
        log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
        synchronized (OBJECT) {
            log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
        }
        log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
    }
}

未加锁时 Mark Word 的最后三位是 001(1),即无锁状态。 加锁时 Mark Word 的最后三位是 000(0),即轻量锁。

3.3.4.4 撤销偏向 -- 两个线程错开时间加锁
@Slf4j(topic = "c.Lock03")
public class Lock03 {

    private static final Object OBJECT = new Object();

    public static void method() {
        log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
        synchronized (OBJECT) {
            log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
        }
        log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
    }

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

        Thread t1 = new Thread(Lock03::method, "t1");
        Thread t2 = new Thread(Lock03::method, "t2");

        t1.start();
        TimeUnit.MILLISECONDS.sleep(500);
        t2.start();

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

通过结果得出结论:

  • 101:偏向状态
  • 101:偏向状态,偏向线程1
  • 101:偏向状态,偏向线程1
  • 101:偏向状态,偏向线程2
  • 000:无偏向,线程2拥有轻量级锁
  • 001:无锁,撤销偏向
3.3.4.5 撤销偏向 -- 调用 wait/notify
@Slf4j(topic = "c.Lock04")
public class Lock04 {
    private static final Object OBJECT = new Object();

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

        Thread t1 = new Thread(() -> {
            log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
            synchronized (OBJECT) {
                log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
                try {
                    OBJECT.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.info(ClassLayout.parseInstance(OBJECT).toPrintable());
        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (OBJECT) {
                OBJECT.notify();
            }
        }, "t2");

        t1.start();
        TimeUnit.MILLISECONDS.sleep(500);
        t2.start();

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

通过结果得出结论:

  • 101:偏向状态
  • 101:偏向状态,偏向线程1
  • 001:无锁,撤销偏向
;