Java多线程 -- 带你了解Java线程synchronized

带你了解Java线程synchronized

0. 前言:Java对象头与Monitor概念

0.1 首先了解下Java对象头:32位虚拟机

  • 普通对象
Object Header (64 bits)
Mark Word (32 bits) | Klass Word (32 bits)
  • 数组对象
Object Header (96 bits)
Mark Word(32bits) | Klass Word(32bits) | array length(32bits)

其中Mark word结构为:

Mark Word (32 bits) State
hashcode : 25 | age: 4 | biased_lock : 0 | 01 Normal
thread : 23 |epoch: 2 | age : 4 | biased_lock : 1 | 01 Biased
ptr_to_lock_record : 30 | 00 Lightweight Locked
ptr_to_heavyweight_monitor : 30 | 10 Heavyweight Locked
| 11 Marked for GC

64位虚拟机Mark word:

Mark Word (32 bits) State
unused : 25 | hashcode : 31 | unused : 1 | age: 4 | biased_lock : 0 | 01 Normal
thread : 54 |epoch: 2 | unused : 1 | age : 4 | biased_lock : 1 | 01 Biased
ptr_to_lock_record : 30 | 00 Lightweight Locked
ptr_to_heavyweight_monitor : 30 | 10 Heavyweight Locked
| 11 Marked for GC

在Mark word中最后两位来判断对象锁级别:

  • 01 : 无锁 或 偏向锁 此时看倒数第三位 0 为无锁 1 为偏向锁
  • 00 : 轻量级锁
  • 10 : 重量级锁
  • 11 : 释放

ptr_to_lock_record : 用来存放指向线程占用该锁的线程地址,下文描述作用。

ptr_to_heavyweight_monitor : 用来存放指向Monitor的地址,下文描述作用。

0.2 其次我们还需要了解Monitor概念,它被称为监视器管程

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针,即上面提到的ptr_to_heavyweight_monitor

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 状态的线程

在了解这些基础好,我们下面再来看看synchronized锁机制。

1. 轻量级锁

应用场景:当多个线程需要对同一个对象加锁,但是加锁的时间是错开的,那么就可以用轻量级锁来优化。

具体例子:有两个方法method1和method2分别对Object加了锁,此时执行main方法。可以发现,有两个线程分别再调用method1和method2,但是,线程Thread-1等待一秒再去获取锁,而此时线程Thread-0已执行完代码块,释放了锁。即两个线程并没有发生竞争,而是将加锁的时间错开了。这时,synchronized使用的是轻量级锁。

public class SynchronizedDemo {
    // 定义一个锁
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            method1();
        },"Thread-0").start();

        new Thread(() -> {
            try {
                Thread.sleep(1_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            method2();
        },"Thread-1").start();
    }

    public static void method1() {
        synchronized (lock) {
            // 执行代码块,但是时间小于1s
        }
    }

    public static void method2() {
        synchronized (lock) {
            // 执行代码块
            // 锁重入
            synchronized (lock) {
                // 执行代码块
            }
        }
    }
}

具体流程:

  • 创建锁记录(Lock Record)对象,每个线程对应的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。

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

  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

  • 当1秒不到时,Thread-0将锁释放,即还是使用cas将Mark word恢复给对象头,Thread-0消亡。而1秒后Thread-1开始获取锁,重复以上步骤。

再继续看method2方法,可以明显发现锁重入,那么锁重入又是怎么样的呢,继续Threa-1步骤看

  • 当Thread-1 执行到第二个synchronized时,即发生锁重入,这是又会进行cas比较并交换。可以显然发现,这时csa一定是失败的。而是在Thread-1中再添加一条 Lock Record 作为重入的计数。

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重
    入计数减一

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

2. 膨胀锁

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

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

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

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

3. 自旋优化

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

4. 偏向锁

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

那有没有办法解决呢,这时偏向锁派上了用场。

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

看看下面的代码:

public class SynchronizedDemo {
    // 定义一个锁
    static final Object lock = new Object();
    
    public static void main(String[] args) {
        new Thread(() -> {
            method1();
        },"thread-0").start();
        
    }

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

    public static void method2() {
        synchronized (lock) {
            
        }
    }
}

发布了17 篇原创文章 · 获赞 1 · 访问量 3418

猜你喜欢

转载自blog.csdn.net/ZeroWdd/article/details/104679113