【多线程学习四】锁原理及wait和notify原理进阶

本文详细介绍了Java对象头在锁机制中的作用,包括监视器/管程、轻量级锁、锁膨胀、自旋优化和偏向锁等概念。解释了synchronized关键字的实现原理,涉及到线程同步、锁的状态转换以及wait/notify机制的工作方式。同时,讨论了锁优化策略,如自旋锁和锁消除,以及如何避免虚假唤醒问题。
摘要由CSDN通过智能技术生成

Monitor

Java对象头,以32位虚拟机为例


在这里插入图片描述
在这里插入图片描述

实现原理

监视器/管程,当使用synchronized给对象上锁时(重量级),对象头的mark word就被设置指向monitor对象的指针
在这里插入图片描述

一个对象指向一个monitor,不会多个对象指向同一个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原理进阶

轻量级锁

一个对象有多个线程要加锁,但是加锁的时间是错开的,可以使用轻量级锁进行优化(00)
在这里插入图片描述

轻量级锁对用户是透明的,语法仍是synchronized

假设有两个临界区用同一个对象锁进行加锁操作

static final Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 	// 同步块 A
 	method2();
 }
}
public static void method2() {
 synchronized( obj ) {
	 // 同步块 B
 }
}
  • 先创建锁记录(lock record)对象,每个线程的栈帧都会包含一个锁记录结构,内部可以存储锁定对象的mark word

在这里插入图片描述

  • 锁记录的Object reference指向锁对象,并尝试用cas替换object的mark word,将替换前object的mark word值存入锁记录

在这里插入图片描述

  • cas替换成功后,object中存储了锁记录地址和状态00,表示该线程已给对象加锁

在这里插入图片描述

  • cas替换失败,有两种情况

    • 其他线程已给该对象加了轻量级锁,此时有竞争,进入锁膨胀过程

    • 自己执行了synchronized锁重入,再添加一条锁记录作为重入的计数

在这里插入图片描述

  • 当退出synchronized代码块时,如果有值为null的锁记录,表示有重入,这时重置锁记录,重入技术减一

在这里插入图片描述

  • 当退出synchronized代码块时,如果没有null的锁记录,使用cas将mark word的值恢复给锁对象

    • 成功,解锁成功
    • 失败,则说明轻量级锁进行了锁膨胀或升级为重量级锁,进入重量级锁解锁流程
缺点

当锁重入时,每次都要用cas去用 锁记录 替换 markword,会影响性能

锁膨胀

当用cas给锁对象加轻量级锁的时候,操作失败,可能是因为已经有其他线程为此对象加上了轻量级锁了,这时就需要进行锁膨胀,将轻量级锁变为重量级锁

static Object obj = new Object();
public static void method1() {
 	synchronized( obj ) {
 		// 同步块
 	}
}
  • 当线程1给锁对象进行轻量级加锁时,线程0已经对该对象加了轻量级锁

在这里插入图片描述

  • 这时线程1给锁对象加锁失败,进入锁膨胀过程

    • 为object申请monitor锁,让object指向重量级锁地址
    • 线程1进入monitor的entrylist blocked

在这里插入图片描述

  • 当线程0解锁后,使用cas将mark word的值恢复给锁对象,失败。这时进入重量级解锁过程,即按照monitor地址找到monitor对象,将owner设置为null,唤醒entrylist中blocked线程

自旋优化

重量级锁竞争时,可以用自旋进行优化,如果当前线程自旋成功(即当前owner值为null,线程释放了锁),则当前线程可以避免阻塞
在这里插入图片描述
在这里插入图片描述

  • 自旋会占用CPU时间,多核自旋才能发挥优势
  • java6+后较为智能,如果对象刚刚的一次自旋操作成功过,会认为这次自旋成功的可能性也高,就会多自旋;反之少自旋/不自旋
  • java7+后不能控制是否开启自旋功能

偏向锁

轻量级锁的优化

java6引入的偏向锁:只有第一次使用cas将线程id设置到对象的mark word中,之后发现该线程id是自己的就表示没有竞争,不用重新cas,之后只要不发生竞争,该对象就归该线程id对应的线程所有
在这里插入图片描述
mark word存的不再是锁记录,而是线程id

在这里插入图片描述

  • 默认开启偏向锁,创建一个对象时,Mark word的值最后3位为101,而此时thread、epoch、age都为0
  • 偏向锁默认是延迟的,不会在程序启动时立即生效,可通过VM参数**-XX:BiasedLockingStartupDelay=0**来禁用延迟
  • 若果没有开启偏向锁,对象创建后mark word值最后3位为001,此时的hashcode、age值为0,第一次用到hashcode才会赋值

偏向锁解锁后,线程id仍存储在对象头中

在项目运行时添加-XX:-UseBiasedLocking禁用偏向锁

为什么打印了对象的hashCode后会禁用了对象的偏向锁?

撤销了对象的偏向状态,为了腾出位置给hashCode存值,需要清除线程id的字节码

调用了hashCode后使用偏向锁,得去掉禁用偏向锁的VM参数

当有其他线程使用偏向锁时,会将偏向锁升级为轻量级锁

调用wait/notify撤销偏向锁

批量重偏向

如果对象被多个线程访问,且没有竞争,这时偏向了线程1的对象仍有机会重新偏向线程2,重偏向会重置对象的线程id

撤销偏向锁的阈值达到了20次后,jvm会给对象加锁时重新偏向至加锁线程

  • 先给对象加上偏向锁,偏向t1
  • 当t1线程执行完后,t2被唤醒,执行t2线程,由于偏向锁中的线程id还是t1的,所以t2执行时会撤销偏向锁,升级为轻量级锁,当撤销的阈值达到了20次后,对象的偏向状态会转给t2,实现重偏向操作
private static void test3() throws InterruptedException {
 	 Vector<Dog> list = new Vector<>();
	 Thread t1 = new Thread(() -> {
	 for (int i = 0; i < 30; i++) {
	 Dog d = new Dog();
	 list.add(d);
	 synchronized (d) {
		 log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
	 }
 }
	 synchronized (list) {
		 list.notify();
	 } 
 }, "t1");
	 t1.start();
 
	 Thread t2 = new Thread(() -> {
	 synchronized (list) {
		 try {
			 list.wait();
		 } catch (InterruptedException e) {
		 e.printStackTrace();
		 }
	 }
	 log.debug("===============> ");
	 for (int i = 0; i < 30; i++) {
		 Dog d = list.get(i);
		 log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
	 synchronized (d) {
	 log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
	 }
	 log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
	 }
	 }, "t2");
	 t2.start();
}
批量撤销

当撤销偏向锁阈值超过40次后,jvm会觉得自己判断失误,不应该偏向,所以整个类的所有对象都会变为不可偏向的,之后新创建的对象也不可偏向的

锁消除

线程默认是启动锁消除优化的,减小加锁带来的性能干扰
在这里插入图片描述

java -XX:-EliminateLocks -jar benchmarks.jar关闭锁消除优化

在这里插入图片描述

Wait/notify

在这里插入图片描述

  • Owner线程发现条件不满足时,调用wait方法,即可进入WaitSet变为WAITING状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  • BLOCKED线程会在Owner释放锁时被唤醒
  • WAITING线程会在Owner调用notify/notifyAll时被唤醒,但唤醒后不意味着立刻获得锁,仍需进入entryList中重新竞争

原理

必须获得此对象的锁才能调用这几个方法

  • obj.wait()让进入object监视器的线程到waitSet等待
  • obj.notify()在object上正在waitSet等待的线程中挑一个唤醒
  • obj.notifyAll()让object上所有在waitSet等待的线程全部唤醒
final static Object obj = new Object();
public static void main(String[] args) {
 	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();
 		// 主线程两秒后执行
 	sleep(2);
 	log.debug("唤醒 obj 上其它线程");
 	synchronized (obj) {
 		obj.notify(); // 唤醒obj上一个线程
		 // obj.notifyAll(); // 唤醒obj上所有等待线程
	 }
}

notify的一种结果

notifyAll的结果

在这里插入图片描述

  • wait()无限制等待,直到notify通知
  • wait(long n) 有时限等待,到n毫秒后结束等待/被notify通知

notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为虚假唤醒

sleep()&wait()

不同

  • sleep是Thread方法,wait是Object方法
  • sleep不会释放锁,wait会释放锁
  • sleep 不用强制与synchronized配合使用,wait需要和synchronized搭配一起使用

相同

sleep和wait状态都是TIMED_WAITING

wait()和notify()的正确姿势

用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了

解决方法,用 while + wait,当条件不成立,再次 wait

new Thread(() -> {
     synchronized (room) {
         log.debug("有烟没?[{}]", hasCigarette);
         while (!hasCigarette) {
             log.debug("没烟,先歇会!");
             try {
             room.wait();
             } catch (InterruptedException e) {
             e.printStackTrace();
             }
    	 }
 	log.debug("有烟没?[{}]", hasCigarette);
     if (hasCigarette) {
     log.debug("可以开始干活了");
     } else {
          log.debug("没干成活...");
     }
     }
	}, "小南").start();
new Thread(() -> {
     synchronized (room) {
         Thread thread = Thread.currentThread();
         log.debug("外卖送到没?[{}]", hasTakeout);
         if (!hasTakeout) {
         	log.debug("没外卖,先歇会!");
             try {
             room.wait();
             } catch (InterruptedException e) {
             e.printStackTrace();
             }
         }
         log.debug("外卖送到没?[{}]", hasTakeout);
         if (hasTakeout) {
         log.debug("可以开始干活了");
         } else {
         log.debug("没干成活...");
         }
     }
	}, "小女").start();
sleep(1);
new Thread(() -> {
     synchronized (room) {
         hasTakeout = true;
         log.debug("外卖到了噢!");
         room.notify();
     }
}, "送外卖的").start();

在这里插入图片描述

模板

synchronized(lock) {
     while(条件不成立) {
     lock.wait();
     }
     // 干活
  }
//另一个线程
synchronized(lock) {
 	lock.notifyAll();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值