synchronized 底层原理 - 04

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 (64 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:62                    | 00  | Lightweight Locked |
|------------------------------------------------------------------|--------------------|
|             ptr_to_heavyweight_monitor:62                  | 10  | Heavyweight Locked |
|------------------------------------------------------------------|--------------------|
|                                                                  | 11 | Marked for GC |
|------------------------------------------------------------------|--------------------|

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状态的线程,后面讲wait-notify 时会分析

锁的状态

在这里插入图片描述

轻量级锁
锁重入

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

  • 轻量级锁对使用者还是透明的,语法仍然是synchronized
static final Object obj = new Object();
    public static  void method1(){
        synchronized (obj)
        {//同步块1
         method2();}}
    private static void method2() {
        synchronized (obj)
        {
            //同步块2}}
  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录结果,内部可以存储锁定对象的Mark Word(标记字段)
    在这里插入图片描述

  • 让锁对象中Object reference 指向,并尝试用cas替换Object的Mark Word,将Mark Word 的值存入锁记录
    在这里插入图片描述

  • 如果cas替换成功,最想头存储了锁记录地址和状态 00 ,表示有该线程给对象加锁
    在这里插入图片描述

  • 如果cas失败

    • 如果是其他线程已经持有该Object的轻量级锁,表明有竞争,进入锁膨胀过程

    • 如果是同一线程执行了 synchronized 锁重入 ,那么在天机一条Lock Record作为锁重入的计数
      在这里插入图片描述

    • 当退出synchronized代码块时(解锁时),如果有取值位null的锁记录,表示有锁重入,只是重置锁记录,表示锁重入数减一
      在这里插入图片描述

    • 当退出synchronized代码块时(解锁时),锁记录部位null ,这时使用cas将 Mark World的值回复给对象头

      • 成功,则结果成功
      • 失败,说明轻量级锁进行了锁膨胀的过程或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
  • 如果尝试加轻量级锁的过程中,CAS无法成功,这是有其他线程未次对象加上了轻量级锁(有竞争),这是需要进行所鹏展,将轻量级锁变成重量级锁。
    • 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
      在这里插入图片描述

    • 这是Thread-1加轻量级锁失败,进入做膨胀过程

      • Object对象申请Monitor锁,让Object指向重量级锁的地址
      • 然后自己进入Moniter的RntryList BOLOCED
        在这里插入图片描述
    • 当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,如果失败,这是会进入重量级锁解锁流程,即按照Monitor地址找到Monitor对象,是指Owner为null ,唤醒EntryList中的BLOCKED线程

自旋优化

重量级锁竞争的时候,还可以用自旋来优化,如果当前线程自旋成功(即这时候持锁线程已经推出了同步块,释放了锁)这时当前线程可以避免阻塞进行上下文切换(上下文切换耗费性能)。
在这里插入图片描述
在这里插入图片描述

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能
偏向锁
  • 轻量级锁在没有竞争时,每次重入依然需要执行CAS操作
  • Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
  • 在这里插入图片描述
偏向状态
|-------------------------------------------------------------------|-------------------|
| 						Mark Word (64 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:62					   | 00 |Lightweight Locked |
|-------------------------------------------------------------------|-------------------|
| 					ptr_to_heavyweight_monitor:62              | 10 |Heavyweight Locked |
|-------------------------------------------------------------------|-------------------|
| 															   | 11 |   Marked for GC   |
|-------------------------------------------------------------------|-------------------|

一个对象创建时:

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

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
原文

  • 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销,在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking

    • 偏向锁不会记录hashcode
    • 轻量级锁会在锁记录中记录 hashCode
    • 重量级锁会在 Monitor 中记录 hashCode
  • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

  • wait/notify 时重量级锁使用的方法 ,调用时偏向锁升级为重量级锁

批量重偏向

以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销操作是,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id改为当前线程ID

  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
  • 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
批量撤销
  • 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
锁消除
  • JIT 即时编译器,???

wait notify 原理

在这里插入图片描述

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
obj.wait()// 让进入 object 监视器的线程到 waitSet 等待
wait(long n) //有时限的等待, 到 n 毫秒后结束等待,或是被 notify
obj.notify() //在 object 上正在 waitSet 等待的线程中挑一个唤醒
obj.notifyAll() //让 object 上正在 waitSet 等待的线程全部唤醒
 final static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();
        // 主线程两秒后执行
        TimeUnit.SECONDS.sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify(); // 唤醒obj上一个线程
            // obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
20:40:04 [Thread-3] c.Test1 - 执行....
20:40:04 [Thread-4] c.Test1 - 执行....
20:40:06 [main] c.Test1 - 唤醒 obj 上其它线程
20:40:06 [Thread-3] c.Test1 - 其它代码....

wait notify 的正确姿势

sleep(long n) 和 wait(long n) 的区别
  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 4) 它们状态 TIMED_WAITING

保护式暂停模式

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式
    在这里插入图片描述
class GuardedObject {
	 private Object response;
	 private final Object lock = new Object();
	 public Object get(Long timeOut) {  //超时时间
		 // 1) 记录最初时间
		long begin = System.currentTimeMillis();
		// 2) 已经经历的时间
		long timePassed = 0;
		synchronized (lock) {
		// 条件不满足则等待
			while (response == null) {   //防止虚假唤醒
				long waitTime = timeOut - timePassed;  //计算剩余等待的时间
				if (waitTime <= 0) {
					log.debug("break...");
					break;
				}
				try {
					lock.wait(waitTime);   //防止虚假唤醒后 等待时间大于输入时间
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// 3) 记录已经经历的时间
				timePassed = System.currentTimeMillis() - begin;
		}
		return response;
	}
 }
 public void complete(Object response) {
		synchronized (lock) {
			// 条件满足,通知等待线程
			this.response = response;
			lock.notifyAll();
		}
 	}
}
 public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            // 子线程执行下载
            List<String> response = download();
            log.debug("download complete...");
            guardedObject.complete(response);
        }).start();
        log.debug("waiting...");
        // 主线程阻塞等待
        Object response = guardedObject.get();
        log.debug("get response: [{}] lines", ((List<String>) response).size());
    }
21:13:15 [main] c.Test1 - waiting...
21:13:17 [Thread-3] c.Test1 - download complete...
21:13:17 [main] c.Test1 - get response: [2] lines

join原理

利用了保护式暂停模式

long totalWaited = 0;
	long totalWaited = 0;    // 记录共计等待时间
	long toWait = timeoutInMilliseconds;  //记录约定等待时间
	boolean timedOut = false;   //记录是否超时

	if (timeoutInMilliseconds == 0 & nanos > 0) {
		// We either round up (1 millisecond) or down (no need to wait, just return)
		if (nanos < 500000)
			timedOut = true;
		else
			toWait = 1;
	}
	while (!timedOut && !isDead()) { //防止虚假环境,并且判断是否超时
		long start = System.currentTimeMillis();  //记录等待开始时间
		wait(toWait);     // 开始等待 第一次等待时间为约定等待时间  第二次为剩余等待时间
		long waited = System.currentTimeMillis() - start; //计算本次循环等待时间
		totalWaited+= waited;  //计算共计等待时间
		toWait -= waited;   //计算等待时间
		// Anyone could do a synchronized/notify on this thread, so if we wait
		// less than the timeout, we must check if the thread really died
		timedOut = (totalWaited >= timeoutInMilliseconds);  //超时判断
	}

在这里插入图片描述

park & Unpark (搁置、推迟)

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park(); 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

示例

Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("park...");
            LockSupport.park();    //线程进行WAIT状态
            log.debug("resume...");
        }, "t1");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        log.debug("unpark...");
        LockSupport.unpark(t1);

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify
park unpark 原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter(计算器) , _cond(状态) 和 _mutex (互斥锁)
在这里插入图片描述

  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量阻塞
  4. 设置 _counter = 0

在这里插入图片描述
5. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
6. 唤醒 _cond 条件变量中的 Thread_0
7. Thread_0 恢复运行
8. 设置 _counter 为 0
在这里插入图片描述
9. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
10. 当前线程调用 Unsafe.park() 方法
11. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
12. 设置 _counter 为 0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1999

每人一点点,明天会更好

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值