题目说明
可重入锁
可重入锁(递归锁)
- ①. 指的是同一线程外层函数获得锁后,再进入该线程的内层方法会自动获取锁 (
前提,锁对象是同一个对象
)
类似于家里面的大门,进入之后可以进入厕所、厨房等 - ②. Java中ReentranLock(显示锁)和synchronized(隐式锁)都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
- ③.
隐式锁:
(即synchronized关键字使用的锁)默认是可重入锁(同步块、同步方法) - 原理如下:
掌握
- 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
- 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1,否则需要等待,直至持有线程释放该锁
- 当执行monitorexit时,Java虚拟机则锁对象的计数器减1。计数器为零代表锁已经被释放
可重入锁四个字分开解释:
可: 可以,重:再次,入:进入,锁:同步锁。进入什么?进入同步域,即同步代码块/方法或显示锁锁定的代码
总结:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁。
可重入锁的种类
- 隐式锁 synchronized关键字使用的锁,默认是可重入锁,同步代码块和同步方法,JVM层面的
- Synchronized的重入的实现机理,自动挡的车
- 显示锁 Lock 也有ReentrantLock这样的可重入锁,手动挡的车
①同步代码块:
//1.同步块
public class SychronizedDemo {
Object object=new Object();
public void sychronizedMethod(){
new Thread(()->{
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t"+"外层....");
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t"+"中层....");
synchronized (object){
System.out.println(Thread.currentThread().getName()+"\t"+"内层....");
}
}
}
},"A").start();
}
public static void main(String[] args) {
new SychronizedDemo().sychronizedMethod();
/*
输出结果:
A 外层....
A 中层....
A 内层....
* */
}
}
②同步方法:
package com.interview.juc;
/**
* Copyright (C), 2018-2020
* FileName: ReenterLockDemo
* Author: kongfanyu
* Date: 2021/1/25 14:20
*/
public class ReenterLockDemo {
public synchronized void m1(){
System.out.println("===外");
m2();
}
public synchronized void m2(){
System.out.println("===中");
m3();
}
public synchronized void m3(){
System.out.println("===内");
}
public static void main(String[] args) {
new ReenterLockDemo().m1();
}
}
从字节码角度分析synchronized实现
- javap -c xxx.class 文件反编译
- synchronized同步代码块
- javap -c xxx.class 文件反编译
- 多出来的一个确保出现异常的时候可以退出,释放锁
- synchronized普通同步方法
- synchronized静态同步方法
Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1.
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1.计数器为零代表锁已被释放。
答案在Java的对象头里。在对象头里,有一块数据叫Mark Word。 在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字 段:锁标志位和占用该锁的thread ID。因为不同版本的JVM实现,对象头的数据结构会有各种差异,此处不再进一步讨论。此处主要是想说明锁实现的思路,因为后面讲ReentrantLock的详细实现时,也基于类似的思路。在这个基本的思路之上,synchronized还会有偏向、自旋等优化策略,ReentrantLock同样会用到这些优化策略,到时会结合代码详细展开。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构:
对象的几个部分的作用:
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间。
显示锁:
package com.interview.juc;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReenterLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread( ()->{
lock.lock();
try {
System.out.println("外层----");
lock.lock();
try
{
System.out.println("内层======");
} finally {
lock.unlock();
}
}finally {
lock.unlock();
}
},"线程A" ).start();
}
}
如果加锁的次数和释放锁的次数不对应,就会出现等待状态,导致其他线程不能执行。
package com.interview.juc;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Copyright (C), 2018-2020
* FileName: ReenterLockDemo
* Author: kongfanyu
* Date: 2020/11/25 14:20
*/
public class ReenterLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread( ()->{
lock.lock();
try {
System.out.println("外层----");
lock.lock();
try
{
System.out.println("内层======");
} finally {
//lock.unlock();//注释之后,线程B不能执行
}
}finally {
lock.unlock();
}
},"线程A" ).start();
//========================
new Thread( () ->{
lock.lock();
try
{
System.out.println("线程B执行.......");
} finally {
lock.unlock();
}
},"线程B").start();
}
}
另一个案例:
//2.同步代码块
class Phone{
public synchronized void sendSms() throws Exception{
System.out.println(Thread.currentThread().getName()+"\tsendSms");
sendEmail();
}
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getName()+"\tsendEmail");
}
}
/**
* Description:
* 可重入锁(也叫做递归锁)
* 指的是同一线程外层函数获得锁后,内层递归函数任然能获取该锁的代码
* 在同一线程外外层方法获取锁的时候,在进入内层方法会自动获取锁
* 也就是说,线程可以进入任何一个它已经标记的锁所同步的代码块
* **/
public class ReenterLockDemo {
/**
* t1 sendSms
* t1 sendEmail
* t2 sendSms
* t2 sendEmail
* @param args
*/
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSms();
} catch (Exception e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
phone.sendSms();
} catch (Exception e) {
e.printStackTrace();
}
},"t2").start();
}
}
显示锁:
(即lock)也有ReentrantLock这样的可重入锁
(注意:有多少个lock,就有多少个unlock,他们是配对使用的;如果多一个或者少一个会使得其他线程处于等待状态)
class Phone2{
static ReentrantLock reentrantLock=new ReentrantLock();
public static void sendSms(){
reentrantLock.lock();
/*
//reentrantLock.lock();
注意有多少个lock,就有多少个unlock,他们是配对使用的
如果多了一个lock(),那么会出现线程B一直处于等待状态
* */
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"\t"+"sendSms");
sendEmails();
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
private static void sendEmails() {
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"\t"+"sendEmails...");
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
public class ReentrantLockDemo {
public static void main(String[] args) {
Phone2 phone2=new Phone2();
new Thread(()->{
phone2.sendSms();},"A").start();
new Thread(()->{
phone2.sendSms();},"B").start();
}
}
LockSupport
LockSupport是什么?
java.util.concurrent.locks
public class LockSupport extends Object
用于创建锁和其他同步类的基本线程阻塞原语。
这个类与每个使用它的线程相关联,一个许可证(在Semaphore类的意义上)。 如果许可证可用,则呼叫park将park返回,在此过程中消耗它; 否则可能会阻止。 致电unpark使许可证可用,如果尚不可用。 (与信号量不同,许可证不能累积,最多只有一个。)
方法park和unpark提供了阻止和解除阻塞线程的有效手段,该方法不会遇到导致不推荐使用的方法Thread.suspend和Thread.resume目的不能使用的问题:一个线程调用park和另一个线程之间的尝试unpark线程将保持活跃性,由于许可证。 另外,如果调用者的线程被中断, park将返回,并且支持超时版本。 park方法也可以在任何其他时间返回,因为“无理由”,因此一般必须在返回之前重新检查条件的循环中被调用。 在这个意义上, park作为一个“忙碌等待”的优化,不浪费时间旋转,但必须与unpark配对才能有效。
park的三种形式也支持blocker对象参数。 线程被阻止时记录此对象,以允许监视和诊断工具识别线程被阻止的原因。 (此类工具可以使用方法getBlocker(Thread)访问阻止程序 。)强烈鼓励使用这些形式而不是没有此参数的原始形式。 在锁实现中作为blocker提供的正常参数是this 。
这些方法被设计为用作创建更高级同步实用程序的工具,并且本身对于大多数并发控制应用程序本身并不有用。 park方法仅用于形式的构造:
while (!canProceed()) {
... LockSupport.park(this); }
其中既不canProceed也没有任何其他动作之前的呼叫park需要锁定或阻止。 因为只有一个许可证与每个线程相关联, park任何中介使用可能会干扰其预期效果。
重点: LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程。类似wait和notify方法。
线程等待唤醒机制(wait/notify)
3种让线程等待和唤醒的方法
- 方法1:使用Object类中的wait方法让线程等待,使用notify方法唤醒线程
- 方法2:使用JUC包中Condition的await方法让线程等待,使用signal方法唤醒线程
- 方法3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
方法1: Object类中的wait和notify方法实现线程等待和唤醒
package com.interview01;
/**
* Copyright (C), 2018-2020
* FileName: Demo1
* Author: kongfanyu
* Date: 2020/12/3 17:30
*/
public class Demo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread( () ->{
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"开始执行");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"被唤醒");
}
},"A").start();
new Thread( () ->{
synchronized (objectLock){
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"发出通知..");
}
},"B").start();
}
}
存在的问题:
①wait和notify不能脱离同步代码块或者同步方法,必须获取监视对象的锁;
②将notify放在wait方法前面,程序无法执行,无法唤醒;可以在线程A中添加sleep(3),让唤醒线程先执行。
//让唤醒线程先执行,就出现问题了,A线程一直处于等待状态,无法唤醒
try {
TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {
}
总结:wait和notify方法必须要在同步块或者方法里面且成对出现使用,先wait后notify才ok。
Codition接口中的await和signal方法实现线程的等待和唤醒
package com.interview01;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Copyright (C), 2018-2020
* FileName: Demo1
* Author: kongfanyu
* Date: 2020/12/3 17:30
*/
public class Demo1 {
static Object objectLock = new Object();
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread( () ->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"开始执行...");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束执行...");
}finally {
lock.unlock();
}
},"A").start();
new Thread( () ->{
lock.lock();
try {
condition.signal();//唤醒
System.out.println(Thread.currentThread().getName()+"开始唤醒...");
}finally {
lock.unlock();
}
},"B").start();
}
}
存在的问题:
①同样需要lock和unlock同步块的支持
②同样如果先唤醒再等待出现一直阻塞问题
传统的synchronized和Lock实现等待唤醒通知的约束:
(A)线程先要获得并持有锁,必须在锁块(synchronized或lock)中,
(B)必须要先等待后唤醒,线程才能被唤醒。
LockSupport类重点 park等待和unpark唤醒
-
是什么
-
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
-
官网解释
public class LockSupport extends Object 用于创建锁和其他同步类的基本线程阻塞原语。 LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是零。 可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1. ------ 这个类与每个使用它的线程相关联,一个许可证(在Semaphore类的意义上)。 如果许可证可用,则呼叫park将park返回,在此过程中消耗它; 否则可能会阻止。 致电unpark使许可证可用,如果尚不可用。 (与信号量不同,许可证不能累积,最多只有一个。)
-
-
主要方法
-
API: 查看文档
-
阻塞: park() / park(Object blocker) 阻塞当前线程/阻塞传入的具体线程
//调用LockSupport.park()时 public static void park(){ UNSAFE.park(false,0L); }
permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。类似停车场的门禁系统。
-
唤醒: unpark(Thread thread) 唤醒处于阻塞状态的指定线程
//调用LockSupport.unpark(thread); public static void unpark(Thread thread){ if(thread != null){ UNSAFE.unpark(thread); } }
调用unpark(thread)方法后,就会将线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park方法会立即返回。
-
-
代码
public static void main(String[] args) { Thread a = new Thread(() ->{ //先让B线程执行,发出通知,A线程还是可以正常结束 try { TimeUnit.SECONDS.sleep(3); } catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"开始执行...."); LockSupport.park();//被阻塞...等待通知放行,它要通过需要有许可证 System.out.println(Thread.currentThread().getName()+"结束执行...."); },"A"); a.start(); //A线程先执行,阻塞,B线程3秒后执行,成功通知A线程。 //try { TimeUnit.SECONDS.sleep(3); } catch (Exception e){ e.printStackTrace(); } Thread b = new Thread(() ->{ LockSupport.unpark(a); System.out.println(Thread.currentThread().getName()+"发出通知...."); },"B"); b.start(); }
①正常+无锁块要求
②之前错误的先唤醒后等待LockSupport照样支持
-
重点说明
LockSupport用于创建锁和其他同步类的基本线程阻塞原语。
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程:
LockSupport和每个使用它的线程都有一个许可permit关联,permit相当于1、0的开关,默认是0,
调用一次unpark就加1,由0变成1,
调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。
形象的理解
线程阻塞需要消耗凭证permit,这个凭证最多只有一个。
当调用park方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
- 如果无凭证,就必须阻塞等待凭证可用;
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
-
面试题
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的消费凭证,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终 结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,凭证不够,故不能放行。