本文源自Recently祝祝,创自Recently祝祝。转载请标注出处。
此解决方式在企业中有所应用,适合Java初级开发学习,参考。
本文字数与行数,耐心阅读必有收获。
1.java锁的分类
上锁方式的不同
显示锁:JUC包中提供的锁,加锁和解锁都是看得见的,在代码中显示的
隐式锁:synchronized(同一时刻最多一个线程执行加锁代码),并不需要显示加锁解锁的过程
锁特性的不同
乐观锁、悲观锁:按照线程时候需要锁住共享资源
乐观锁:无论什么时候都保持乐观,觉得不会有别的线程修改数据,获取数据的时候不上锁,若在获取数据时存在其他线程更新,则报错或者自动重试等操作。CAS算法、关系型数据库的版本号机制
悲观锁:任何时候都要加锁,保证数据不被修改。JUC的锁、Synchronized
可重入锁、不可重入锁:是否能重复上锁同一个线程资源
可重入锁:重复加锁,同样的资源重复锁定 ,锁次数增加1.释放资源则减1。可重入锁的一个优点是可一定程度避免死锁。不用等待锁释放,可以直接获取锁。ReentrantLock、synchronized
不可重入锁:不能对同样的资源重复加锁,重复获取锁则发生死锁。
公平锁、非公平锁:多线程竞争同一锁资源的时候需不需排队
公平锁:线程需要按照申请锁的顺序获取锁
非公平锁:不需要按照申请锁的顺序获取锁。
独享锁、共享锁:多个线程是否能共享同一把锁
独享锁(排他锁,写锁):同一时间只能有一个线程获取锁,其他线程将被阻塞,等待锁释放之后才能获取锁
共享锁(读锁):多个线程可以同时获取同一把锁,无需等待。
其他锁
自旋锁:
不加锁,通过指令,实现原子性可见性,多个线程操作,先停一会给其他线程操作,其他线程操作完了,再去获取资源执行。不断循环尝试获取锁,直到获取锁成功。
分段锁:
锁不是锁住整个资源,而是锁住其中的一部分,多线程访问容器里不同数据段的数据,线程间不存在锁竞争,提高并发访问效率
无锁(不加锁)、偏量锁(只有一个线程)、轻量级锁(两个线程就是用CAS)、重量级锁(多个线程资源复杂的时候使用):
四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
分析以上锁特征可以知道synchronized分别是:隐式锁、可重入锁、非公平锁、独享锁、排他锁、悲观锁。
2.重点说明:重入锁:ReentrantLock
可重入锁,重入锁,分布式锁、手写锁跟重入锁逻辑相似,重入锁可以是公平的也可以是非公平的,核心就是加锁和解锁,
核心方式:
类图:
源码图:
NonfairSync和FairSync都继承自抽象类Sync,而Sync类继承自抽象类AbstractQueuedSynchronizer(简称AQS)。在ReentrantLock中有非公平锁NonfairSync和公平锁FairSync的实现
定义重入锁可以设置公平还是不公平,默认公平。
第一点:加锁,线程抢夺锁的实现;第二点:解锁,线程被唤醒实现
private Lock lock = new ReentrantLock();
1)ReentrantLock源码分析--锁获取
AQS实现源码
//AQS内部维护这一个双向链表,AQS主要属性
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient volatile Node head;//头节点指针
private transient volatile Node tail;//尾节点指针
private volatile int state;//同步状态,0无锁;大于0,有锁,state的值代表重
入次数。
//AQS链表节点结构
static final class Node {
static final Node SHARED = new Node();//共享模式
static final Node EXCLUSIVE = null;//独占模式
/**
*等待状态:取消。表明线程已取消争抢锁并从队列中删除。
*取消动作:获取锁超时或者被其他线程中断。
*/
static final int CANCELLED = 1;
/**
*等待状态:通知。表明线程为竞争锁的候选者。
*只要持有锁的线程释放锁,会通知该线程。
*/
static final int SIGNAL = -1;
/**
*等待状态:条件等待
*表明线程当前线程在condition队列中。
*/
static final int CONDITION = -2;
/**
*等待状态:传播。
*用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机
制。
*在一个节点成为头节点之前,是不会跃迁为此状态的
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;//直接前驱节点指针
volatile Node next;//直接后继节点指针
volatile Thread thread;//线程
Node nextWaiter;//condition队列中的后继节点
final boolean isShared() {//是否是共享
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {//默认构造器
}
//在重入锁中用于addWaiter方法中,用于将阻塞的线程封装成一个Node
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {//用于Condition中
this.waitStatus = waitStatus;
this.thread = thread;
}
}
}
场景01-线程抢夺锁失败时,AQS队列的变化【加锁】
① AQS的head、tail分别代表同步队列头节点和尾节点指针默认为null
② 当第一个线程抢夺锁失败,同步队列会先初始化,随后线程会被封装成Node节点追加到AQS队列中。
➢ 假设:当前独占锁的的线程为ThreadA,抢占锁失败的线程为ThreadB。
➢ 2.1 同步队列初始化,首先在队列中添加Node,thread=null
➢ 2.2 将ThreadB封装成为Node,追加到AQS队列
③ 当下一个线程抢夺锁失败时,继续重复上面步骤。假设:ThreadC抢占线程失败
源码解析
可以设置重入锁公平或者不公平
默认公平锁
ReentrantLock.lock()
sync是Sync类的一个实例,Sync类实际上是ReentrantLock的抽象静态内部类,它集成了AQS来实现重入锁的具体业务逻辑。AQS是一个同步队列,实现了线程的阻塞和唤醒,没有实现具体的业务功能。在 不同的同步场景中,需要用户继承AQS来实现对应的功能。
第一个公平锁
线程进入AQS中的acquire方法,arg=1。
这个方法逻辑:先尝试抢占锁,抢占成功,直接返回;
抢占失败,将线程封装成Node节点追加到AQS队列中并使线程阻塞等待。
1)首先会执行tryAcquire(1)尝试抢占锁,成功返回true,失败返回false。抢占成功了,就不会执行下面的代码了
(2)抢占锁失败后,执行addWaiter(Node.EXCLUSIVE)将x线程封装成Node节点追加到AQS队列。
(3)然后调用acquireQueued将线程阻塞,线程阻塞。 线程阻塞后,接下来就只需等待其他线程唤醒它,线程被唤醒后会重新竞争锁的使用。
FairSync.tryAcquire(1)
尝试获取锁:若获取锁成功,返回true;获取锁失败,返回false。
这个方法逻辑:获取当前的锁状态,如果为无锁状态,当前线程会执行CAS操作尝试获取锁;若当前线程是重入获取锁,只需增加锁的重入次数即可。
点击应用
AQS.addWaiter(Node.EXCLUSIVE)
线程抢占锁失败后,执行addWaiter(Node.EXCLUSIVE)将线程封装成Node节点追加到AQS队列。
addWaiter(Node mode)的mode表示节点的类型,Node.EXCLUSIVE表示是独占排他锁,也就是说重入锁是独占锁,用到了AQS的独占模式。
Node定义了两种节点类型:
- 共享模式:Node.SHARED。共享锁,可以被多个线程同时持有,如读写锁的读锁。
- 独占模式:Node.EXCLUSIVE。独占很好理解,是自己独占资源,独占排他锁同时只能由一个线程持有。
AQS.acquireQueued(newNode,1)
这个方法的主要作用就是将线程阻塞。
- 若同步队列中,若当前节点为队列第一个线程,则有资格竞争锁,再次尝试获得锁。
- 尝试获得锁成功,移除链表head节点,并将当前线程节点设置为head节点。
- 尝试获得锁失败,判断是否需要阻塞当前线程。
- 若发生异常,取消当前线程获得锁的资格。
AQS.shouldParkAfterFailedAcquire
这个方法的主要作用是:线程竞争锁失败以后,通过Node的前驱节点的waitStatus状态来判断, 线程是否需要被阻塞。
- 如果前驱节点状态为 SIGNAL,当前线程可以被放心的阻塞,返回true。
- 若前驱节点状态为CANCELLED,向前扫描链表把 CANCELLED 状态的节点从同步队列中移除,返回false。
- 若前驱节点状态为默认状态或PROPAGATE,修改前驱节点的状态为 SIGNAL,返回 false。
- 若返回false,会退回到acquireQueued方法,重新执行自旋操作。自旋会重复执行
acquireQueued和shouldParkAfterFailedAcquire,会有两个结果:
(1)线程尝试获得锁成功或者线程异常,退出acquireQueued,直接返回。
(2)执行shouldParkAfterFailedAcquire成功,当前线程可以被阻塞。 - 若返回true,调用parkAndCheckInterrupt阻塞当前线程。
Node 有 5 种状态,分别是:
- 0:默认状态。
- 1:CANCELLED,取消/结束状态。表明线程已取消争抢锁。线程等待超时或者被中断,节点的waitStatus为CANCELLED,线程取消获取锁请求。需要从同步队列中删除该节点
- -1:SIGNAL,通知。状态为SIGNAL节点中的线程释放锁时,就会通知后续节点的线程。
- -2:CONDITION,条件等待。表明节点当前线程在condition队列中。
- -3:PROPAGATE,传播。在一个节点成为头节点之前,是不会跃迁为PROPAGATE状态的。用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制。
获取锁成功跳出循环返回,获取锁失败,就阻塞等待,等待上一个节点释放锁
AQS.parkAndCheckInterrupt
将当前线程阻塞挂起。 LockSupport.park(this)会阻塞当前线程,会使当前线程(如ThreadB)处于等待状态,不再往下执行。
2)ReentrantLock源码分析--锁释放
场景02-线程被唤醒时,AQS队列的变化【解锁】
① ReentrantLock唤醒阻塞线程时,会按照FIFO的原则从AQS中head头部开始唤醒首个节点中线程。
② head节点表示当前获取锁成功的线程ThreadA节点。
③ 当ThreadA释放锁时,它会唤醒后继节点线程ThreadB,ThreadB开始尝试获得锁,如果ThreadB获得锁成功,会将自
己设置为AQS的头节点。ThreadB获取锁成功后,AQS变化如下:
源码解析:
公平不公平释放锁都是同一个tryRelese()
ReentrantLock.unlock
释放锁时,需调用ReentrantLock的unlock方法。这个方法内部,会调sync.release(1),release方法为AQS类的final方法。
AQS.release(1)
首先执行方法tryRelease(1),tryRelease方法为ReentrantLock中Sync类的final方法,用于释放锁。
- 释放锁。若释放后锁状态为无锁状态,需唤醒后继线程
- 同步队列头节点
- 若head不为null,说明链表中有节点。其状态不为0,唤醒后继线程。
第三步:Sync.tryRelease(1)
- 判断当前线程是否为锁持有者,若不是持有者,不能释放锁,直接抛出异常。
- 若当前线程是锁的持有者,将重入次数减1,并判断当前线程是否完全释放了锁。
-
- 若重入次数为0,则当前新线程完全释放了锁,将锁拥有线程设置为null,并将锁状态置为无锁状态(state=0),返回true。
- 若重入次数>0,则当前新线程仍然持有锁,设置重入次数=重入次数-1,返回false。
- 返回true说明,当前锁被释放,需要唤醒同步队列中的一个线程,执行unparkSuccessor唤醒同步队列中节点线程。
AQS.unparkSuccessor
- 唤醒后继线程
- 头节点waitStatus状态 SIGNAL或PROPAGATE
- 查找需要唤醒的节点:正常情况下,它应该是下一个节点。但是如果下一个节点为null或者它的 waitStatus为取消时,则需要从同步队列tail节点向前遍历,查找到队列中首个不是取消状态的节点。
- 将下一个节点中的线程unpark唤醒
LockSupport.unpark(s.thread)
会唤醒挂起的线程,使被阻塞的线程继续执行。unpark相当于notify();