可重入锁
①. 指的是同一线程外层函数获得锁后,再进入该线程的内层方法会自动获取锁 (前提,锁对象是同一个对象)
类似于家里面的大门,进入之后可以进入厕所、厨房等
②. Java中ReentranLock(显示锁)和synchronized(隐式锁)都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
③. 隐式锁:(即synchronized关键字使用的锁)默认是可重入锁(同步块、同步方法)
原理如下:掌握
- 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
- 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1,否则需要等待,直至持有线程释放该锁
- 当执行monitorexit时,Java虚拟机则锁对象的计数器减1。计数器为零代表锁已经被释放
同步块
/**
* 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,
* 这样的锁就叫做可重入锁。
*
* 在一个synchronized修饰的方法或代码块的内部
* 调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEnterLockDemo {
static Object objectLockA = new Object();
public static void m1(){
new Thread(() -> {
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"------外层调用");
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"------中层调用");
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"\t"+"------内层调用");
}
}
}
},"t1").start();
}
public static void main(String[] args) {
m1();
/*
输出结果:
A 外层....
A 中层....
A 内层....
* */
}
}
同步方法:
/**
* 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
*
* 在一个synchronized修饰的方法或代码块的内部
* 调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
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的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锋对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加i。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
ReentrantLock 实现的可重入锁
/**
* 可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
*
* 在一个synchronized修饰的方法或代码块的内部
* 调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEnterLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
//lock.lock();
try{
System.out.println("=======外层");
lock.lock();
try{
System.out.println("=======内层");
}finally {
lock.unlock();
}
} finally {
//实现加锁次数和释放次数不一样
//由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock();
//lock.unlock(); //正在情况,加锁几次就要解锁几次
}
},"t1").start();
new Thread(() -> {
lock.lock();
try{
System.out.println("b thread----外层调用lock");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
},"b").start();
}
}
LockSupport
为什么要使用LockSupport(先来了解下传统的等待唤醒机制)
1、![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/32f8ac0f7fea8d7189ae056a5ae39993.png#pic_center)
2、Object类中wait( )和notify( )实现线程的等待唤醒
- wait和notify方法必须要在同步块或同步方法里且成对出现使用。 wait和notify方法两个都去掉同步代码块后看运行效果出现异常情况:
Exception in thread “A” Exception in thread “B”
java.lang.IllegalMonitorStateException - 先wait后notify才可以(如果先notify后wait会出现另一个线程一直处于等待状态)
- synchronized是关键字属于JVM层面。monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只能在同步块或方法中才能调用wait/notify等方法)
private static void synchronizedWaitNotify() {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "..come in");
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();
}
3、Condition接口中的await和signal方法实现线程等待和唤醒
(出现的问题和object中wait和notify一样)
private static void lockAwaitSignal() {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "..come in");
try {
condition.await();
} catch (Exception 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();
}
总结:
1、wait()和notify()不能脱离同步代码块 IllegalMonitorStateException
2、将notify()放在wait()前面,程序无法执行,无法唤醒
1、await()和signal()不能脱离lock,即线程要先获得并持有锁,必须在锁块中
2、必须先等待后唤醒,线程才能被唤醒
JUC三个工具类
CountDownLatch | CyclicBarrier | Semaphore 底层都是AQS来实现的
①. CountDownLatch
-
①. CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
-
②. 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
-
③. 计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
//需求:要求6个线程都执行完了,mian线程最后执行
public class CountDownLatchDemo {
public static void main(String[] args) throws Exception{
CountDownLatch countDownLatch=new CountDownLatch(6);
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t");
countDownLatch.countDown();
},i+"").start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t班长关门走人,main线程是班长");
}
}
- ④. 利用枚举减少if else的判断
public enum CountryEnum {
one(1,"齐"),two(2,"楚"),three(3,"燕"),
four(4,"赵"),five(5,"魏"),six(6,"韩");
private Integer retCode;
private String retMessage;
private CountryEnum(Integer retCode,String retMessage){
this.retCode=retCode;
this.retMessage=retMessage;
}
public static CountryEnum getCountryEnum(Integer index){
CountryEnum[] countryEnums = CountryEnum.values();
for (CountryEnum countryEnum : countryEnums) {
if(countryEnum.getRetCode()==index){
return countryEnum;
}
}
return null;
}
public Integer getRetCode() {
return retCode;
}
public String getRetMessage() {
return retMessage;
}
}
/*
楚 **国,被灭
魏 **国,被灭
赵 **国,被灭
燕 **国,被灭
齐 **国,被灭
韩 **国,被灭
main **秦国一统江湖
* */
public class CountDownLatchDemo {
public static void main(String[] args) throws Exception{
CountDownLatch countDownLatch=new CountDownLatch(6);
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"**国,被灭");
countDownLatch.countDown();
},CountryEnum.getCountryEnum(i).getRetMessage()).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t"+"**秦国一统江湖");
}
}
②. CyclicBarrier
-
①. CyclicBarrier的字面意思是可循环(Cyclic) 使用的屏障(barrier).它要做的事情是,让一组线程到达一个屏障(也可以叫做同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法
-
②. 代码验证:
//集齐7颗龙珠就能召唤神龙
public class CyclicBarrierDemo {
public static void main(String[] args) {
// public CyclicBarrier(int parties, Runnable barrierAction) {}
CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
System.out.println("召唤龙珠");
});
for (int i = 1; i <=7; i++) {
final int temp=i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t收集到了第"+temp+"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
③.Semaphore(信号量)
-
①. acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
-
②. release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
-
③. 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
-
④. 代码验证
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(3);
for (int i = 1; i <=6; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"\t抢占了车位");
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
④. LockSupport
- ①. 什么是LockSupport?
1、通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
2、LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
3、官网解释:
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零
可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1
- ②. 阻塞方法
1、permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时, park方法会被唤醒,然后会将permit再次设置为0并返回。
2、static void park( ):底层是unsafe类native方法
3、static void park(Object blocker)
public static void park() {
UNSAFE.park(false, 0L);
}
- ③.唤醒方法(注意这个permit最多只能为1)
1、调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回
2、static void unpark( )
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
- ④. LockSupport它的解决的痛点
1、LockSupport不用持有锁块,不用加锁,程序性能好
2、先后顺序,不容易导致卡死(因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞) - ⑤. 代码演示:
/*
(1).阻塞
(permit默认是O,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,
park方法会被唤醒,然后会将permit再次设置为O并返回)
static void park()
static void park(Object blocker)
(2).唤醒
static void unpark(Thread thread)
(调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,
permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回)
static void unpark(Thread thread)
* */
public class LockSupportDemo {
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
LockSupport.park();
/*
如果这里有两个LockSupport.park(),因为permit的值为1,上一行已经使用了permit
所以下一行被注释的打开会导致程序处于一直等待的状态
* */
//LockSupport.park();
System.out.println(Thread.currentThread().getName()+"\t"+"被B唤醒了");
},"A");
t1.start();
//下面代码注释是为了A线程先执行
//try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
Thread t2=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程");
//有两个LockSupport.unpark(t1),由于permit的值最大为1,所以只能给park一个通行证
LockSupport.unpark(t1);
//LockSupport.unpark(t1);
},"B");
t2.start();
}
}
- ⑥. 面试题目
为什么可以先唤醒线程后阻塞线程?
因为unpark()获得了一个凭证,之后再调用park()方法,就可以名正言顺的凭证消费,故不会阻塞
为什么唤醒两次后阻塞两次,但最终结果还是会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark()和调用一次unpark()效果一样,只会增加一个凭证; 而调用两次park却需要消耗两个凭证,证不够,不能放行。
AQS
①. AQS是什么?
- ① AQS:是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,
通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量
表示持有锁的状态 - AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
② AQS内部框架图
- ① AQS内部框架图
- ②. 详解AQS内部代码有什么?
- ③. CLH队列(三个大牛的名字组成),为一个双向队列
- ④. 内部结构(Node此类的讲解)
- ⑤. 属性说明(Node此类的讲解)
- ⑥. AQS同步队列的基本结构
③. ReentrantLock开始解读AQS
写在最前面: (1). 本次讲解我们走最常用的,lock/unlock作为案例突破口 (2).
我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?3个状态:没占用是0,占用了是1,大于1是可重入锁 (3).
如果AB两个线程进来了以后,请问这个总共有多少个Node节点?答案是3个,其中队列的第一个是傀儡节点(哨兵节点) 业务图:
①.CODE
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
//3个线程模拟3个来银行网点,受理窗口办理业务的顾客
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(() -> {
lock.lock();
try{
System.out.println("-----A thread come in");
try { TimeUnit.MINUTES.sleep(20); }catch (Exception e) {e.printStackTrace();}
}finally {
lock.unlock();
}
},"A").start();
//第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try{
System.out.println("-----B thread come in");
}finally {
lock.unlock();
}
},"B").start();
//第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try{
System.out.println("-----C thread come in");
}finally {
lock.unlock();
}
},"C").start();
}
}
②. 从最简单的lock方法开始看看公平和非公平
可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()
hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
③. 非公平锁lock()
①. lock()方法
②. acquire( ):源码和3大流程走向
④. tryAcquire(arg)
-
①.本次走非公平锁方向
-
②. nonfairTryAcquire(acquires)
return false(继续推进条件,走下一步方法addWaiter)
return true(结束)
⑤. addWaiter(Node.EXCLUSIVE)
- ①. addWaiter(Node mode )
双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的
- ②. enq(node);
- ③. B、C线程都排好队了效果图如下:
⑥. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
-
①. acquireQueued
(会调用如下方法:shouldParkAterFailedAcquire和parkAndCheckInterrupt | setHead(node) ) -
②. shouldParkAfterFailedAcquire
-
③. parkAndCheckInterrupt
-
④. 当我们执行下图中的③表示线程B或者C已经获取了permit了
-
⑤. setHead( )方法
代码执行完毕后,会出现如下图所示
⑦. unlock( )获取permit
- ①. release | tryRelease | unparkSuccessor(h);
- ②. tryRelease()
- ③. unparkSuccessor( )