目录
一. Lock 基础
- Lock 锁与 synchronized 的区别
- 两种锁都是重入锁
- synchronized 是关键字,属于jvm层面(底层通过monitor对象完成,其实 wait/notify等方法也依赖于monitor对象)
- Lock 是 concurrent 并发包下的具体类 api层面的锁
- synchronized在代码执行完毕,或抛出异常时会自动释放锁,Lock锁在代码执行完毕或抛出异常不会自动释放锁,需要手动调用unlock()方法手动释放锁
- synchronized 非公平锁,Lock 锁默认是非公平锁,例如创建ReentrantLock锁时构造器中传递true,则使用公平锁
- Lock 可以调用tryLocak() 方法去尝试获取锁,如果返回true则说明获取到了锁,如果返回false则说明当前不能获取锁
- Lock 锁可以绑定 Condition 实现对线程的精准阻塞与唤醒,而使用 synchronized 配合 wait() 与 notify使用时只能随机唤醒一个,或者唤醒全部
- Lock 常用方法
//如果构造器为空创建的是非公平锁,如果构造器中传递true创建的是公平锁
private ReentrantLock lock = new ReentrantLock();
//获取锁
lock.lock();
//尝试获取锁,返回获取成功或失败的布尔值
boolean b = lock.tryLock();
//尝试获取锁可以指定等待获取的时间,指定时间内阻塞等待
lock.tryLock(2000l, TimeUnit.SECONDS);
//获取一个可中断锁
lock.lockInterruptibly();
//中断锁的获取
Thread.currentThread().interrupt();
- 代码示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
//创建Lock对象锁,(ReentrantLock是Lock接口的实现)
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
//使用final修饰,是因为匿名内部类中不允许有变量
final LockTest lockTest = new LockTest();
//线程1
/*匿名内部类方式创建子线程,并给子线程命名*/
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lockTest.method(Thread.currentThread());
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockTest.method(Thread.currentThread());
}
}, "t2");
t1.start();
t2.start();
}
//需要参与同步的方法
private void method(Thread thread) {
/*示例1,lock()方法获取锁
//获取锁,如果获取不到,则一直等待
lock.lock();
try {
System.out.println("线程名"+thread.getName() + "获得了锁");
}catch(Exception e){
e.printStackTrace();
} finally {
System.out.println("线程名"+thread.getName() + "释放了锁");
//由于Lock方式获取到的锁需要手动释放,需要使用try--catch在finally
//中手动释放锁,防止代码报错时,代码无法正常执行,锁无法释放
lock.unlock();
}*/
//tryLock(),获取锁,如果获取到锁,返回boolean值true否则为false
//可以根据是否获取到锁进行指定操作
//相同还设有tryLock(long timeout,TimeUnit unit)方法,如果获取到了锁返回true
//如果没有获取到锁会等待指定时间unit,如果这个时间内还是获取不到锁返回false
/*示例2,tryLock()方法获取锁, 此方法的执行最终都在finally中释放了锁*/
if (lock.tryLock()) {
try {
System.out.println("线程名" + thread.getName() + "获得了锁");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("线程名" + thread.getName() + "释放了锁");
lock.unlock();//释放锁
}
} else {
System.out.println("我是" + Thread.currentThread().getName() + "有人占着锁");
}
}
}
二. 通过 队列 + Unsafe 自定义一个简单版的锁
- 步骤:
- 提供一个锁是否被获取的标识
- 提供一个队列,利用队列先进先出特性,存储锁被获取后其他等待的线程
- 锁以被其它线程获取没有释放时,通过 Unsafe 的 park() 设置当前线程进入等待
- 释放锁时,在队列中取出最先等待的线程,通过 Unsafe 的 unpark(thread) 唤醒指定线程继续执行
- 自定义锁示例
//自定义锁
class MyLock {
//1.通过 state 判断标识锁是否以被获取,0为没有正常执行,加锁设置为1,释放锁时设置为0
private static int state;
//2.利用队列先进先出的特性,当有判断锁以被其它线程获取时,当前线程阻塞
//存入该队列中,后续锁被释放后由该队列中取出头部元素也就是最先添加的线程唤醒执行
private static Queue<Thread> queue= new LinkedBlockingQueue();
//获取锁方法
public static void lock() throws Exception {
//1.判断 state 如果为0表示没有线程获取锁,跳过 while循环
while (state!=0){
//2.当锁被其它线程获取后,将当前线程存入队列中
boolean b = queue.offer(Thread.currentThread());
if(!b){
throw new Exception();
}
//3.设置当前线程进入等待状态 LockSupport 内部封装的 Unsafe
//实际调用的是 Unsafe 的 park()方法,被该方法设置等待的线程
//需要通过 Unsafe 调用 unpark(Thread) 进行指定唤醒
LockSupport.park();
}
//state 设置为1表示当前线程获取锁
state = 1;
}
//释放锁
public static void releaseLock(){
//1.state锁标识设置为0,
state = 0;
//2.获取队列中头部元素(队列先进先出,也就是最先存入的等待线程)
Thread thread = queue.poll();
if(null != thread){
//3.通过 Unsafe 调用 unpark() 方法唤醒指定线程继续执行
LockSupport.unpark(thread);
}
}
}
- 通过多线程调用业务代码,业务代码执行需要获取锁,运行测试
public class RunTest{
public static void main(String[] args) {
//线程 A 执行业务代码 testRun()
new Thread(()->{
try {
testRun();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
//线程 B 执行业务代码 testRun()
new Thread(()->{
try {
testRun();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
//业务方法,
public static void testRun() throws Exception {
//1.获取锁
MyLock.lock();
//======================2.模拟业务代码执行==========================================
System.out.println(Thread.currentThread().getName()+"线程获取到锁开始开始执行");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+"线程执行完毕,释放锁");
//======================模拟业务代码执行 end======================================
//3.释放锁
MyLock.releaseLock();
}
}
- 思考问题: 假设当前T1线程获取到锁正在执行未释放,在这个过程中 T2.T3 等多个线程进来,判断锁被其它线程获取,将T2,T3线程存入队列中等待,假设T1线程执行完毕释放锁,在释放锁到唤醒队列中其它线程的过程中,T4线程执行,判断锁没有被获取,会直接执行T4线程,而队列中等待的T2,T3线程还是在队列中继续等待,这也就是公平锁解决的问题
三. ReentrantLock 底层源码分析
- ReentrantLock 底层获取锁与释放锁不考虑细节,跟上面自定义锁的原理差不多,ReentrantLock 依赖 AbstractQueuedSynchronizer 可以看为是一个队列,通过state表示锁状态,通过 Node 内部类,保存阻塞线程数据,提供了头部与尾部属性
- ReentrantLock 实现 Lock 与 Serializable 接口,可重入锁,调用空参构造器默认创建非公平锁,传递true时创建公平锁,查看源码会发现通过两个内部类 FairSync 公平,NonfairSync非公平来实现的,这两个内部类又继承了 Sync 内部类,Sync 内部类继承了 AbstractQueuedSynchronizer 抽象类
//非公平锁
public ReentrantLock() {
this.sync = new ReentrantLock.NonfairSync();
}
//公平锁
public ReentrantLock(boolean var1) {
this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}
abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends ReentrantLock.Sync
static final class FairSync extends ReentrantLock.Sync
- AbstractQueuedSynchronizer 重要组成部分
- AbstractOwnableSynchronizer 父类中 transient Thread exclusiveOwnerThread: 当前持有锁的线程
- volatile int state: 标识同步状态
- volatile Node head: 当前整个队列中存储的第一个节点数据
- volatile Node tail: 当前整个队列中存储的最后个节点数据
- class Node: 内部类
- volatile Node prev: 上一个节点
- volatile Node next: 下一个节点
- volatile Thread thread;: 该节点上保存的线程
//等待队列的头,延迟初始化。除初始化外,只能通过setHead方法进行修改。
//注意:如果head存在,则保证其waitStatus不为 CANCELLED。
private transient volatile Node head;
//等待队列的尾部,延迟初始化。仅通过方法enq进行修改以添加新的等待节点
private transient volatile Node tail;
//同步状态。
private volatile int state;
static final class Node{
/** 指示节点正在共享模式下等待的标记 */
static final Node SHARED = new Node();
/**指示节点正在以独占模式等待的标记*/
static final Node EXCLUSIVE = null;
/** waitStatus值,指示线程已取消 */
static final int CANCELLED = 1;
/** waitStatus值,指示后续线程需要释放*/
static final int SIGNAL = -1;
/**waitStatus值,指示线程正在等待条件 */
static final int CONDITION = -2;
/**waitStatus值,指示下一个acquireShared应该*无条件传播*/
static final int PROPAGATE = -3;
/**
* 状态字段,仅采用以下值:
* SIGNAL: 该节点的后继者被(或即将被)(通过停放)被阻止,
* 因此当前节点在释放或取消时必须取消其后继者的停放。
* 为避免种族冲突,acquire方法必须首先表明它们需要信号,
* 然后重试原子获取,然后失败时阻塞。
*
* CANCELLED: 由于超时或中断,该节点被取消。节点永远不会离开此状态。
* 特别是具有取消节点的线程永远不会再次阻塞
*
* CONDITION: 该节点当前在条件队列中。在传输之前,
* 它不会用作同步队列节点,此时状态*将设置为0。
* (此处使用此值与字段的其他用法无关,但简化了机制)
*
* PROPAGATE: releaseShared应该传播到其他节点。
* 在doReleaseShared中设置了此设置(仅适用于头节点),
* 以确保传播继续进行,即使由于干预而进行了其他操作。
*
* 0: 以上都不是
*
* 这些值以数字方式排列以简化使用。非负值表示节点无需发送信号。
* 因此,大多数代码不需要检查特定的*值,仅需检查符号即可
*
* 对于普通同步节点,该字段初始化为0,对于条件节点,该字段初始化为CONDITION。
* 使用CAS(或在可能时进行无条件的易失性写操作)对其进行修改
*/
volatile int waitStatus;
/**
* 链接到当前节点/线程所依赖的先前节点*,以检查waitStatus。在排队时分配,
* 并且仅在出队时将清空(出于GC的考虑)。同样,在取消前任后,我们会在短路的同时
* 找到一个永远不会存在的不可取消的前者,因为头节点从未被取消:一个节点仅由于成功
* 获取而成为头。被取消的线程永远不会成功获取,只有一个线程会取消自身,而不会取消任何其他节点。
*/
volatile Node prev;
/**
* 链接到当前节点/线程释放后将停放的后继节点。在入队期间分配,在绕过已取消的前任时进行调整,
* 并在出队时清零(出于GC的考虑)。 enq操作在附件之后才*分配前任的下一个字段,因此看到下一个
* 字段为空并不一定意味着节点位于队列末尾。但是,如果下一个字段为空,则我们可以从尾部扫描上一个
* 进行双重检查。取消节点的下一个字段设置为指向节点本身而不是null,以使isOnSyncQueue的工作更轻松。
*/
volatile Node next;
/**
* 使该节点排队的线程。在结构上初始化,使用后消失。
*/
volatile Thread thread;
/**
* 链接到等待条件的下一个节点,或者共享特殊值。因为条件队列仅在处于独占模式时才被访问,
* 所以我们只需要一个简单的链接队列即可在节点等待条件时保存节点。然后将它们转移到队列以重新获取。
* 由于条件只能是互斥的,因此我们使用特殊值来表示共享模式来保存字段。
*/
Node nextWaiter;
/**
* 如果节点在共享模式下等待,则返回true。
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回上一个节点,如果为null,则抛出NullPointerException,在前任不能为null时使用。
* 可以省略null检查,但是它可以帮助VM
*
* @return 该节点的前身
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // 用于建立初始标头或SHARED标记
}
Node(Thread thread, Node mode) { // 由addWaiter使用
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 根据条件使用
this.waitStatus = waitStatus;
this.thread = thread;
}
}
ReentrantLock 中 lock() 获取锁方法
-
调用lock() 方法时,方法内部调用 Sync 内部类的 lock() 方法,Sync中 lock() 方法是个抽象方法,实际是根据创建的的 ReentrantLock 是公平还是非公平,选择调用 FairSync 公平,或 NonfairSync非公平 Sync 子类的 lock 方法
-
FairSync 中的 lock() 方法
-
NonfairSync 中的 lock() 方法
final void lock() {
//首先会尝试修改state 锁状态,当前线程继续执行,如果修改失败
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//AbstractQueuedSynchronizer 的 acquire() 方法,而 FairSync 公平锁是先执行该方法去排队
acquire(1);
}
FairSync 公平锁获取流程
- FairSync 中的 lock 方法中调用了AbstractQueuedSynchronizer 父类的 acquire() 方法,
- acquire() 方法中执行 “!tryAcquire(arg)”,判断锁是否被其它线程获取,有两种情况
- 当前锁没有被其它线程获取 tryAcquire(arg) 返回true,取反 false ,如果没有被其它线程获取会一并设置state为已锁状态,不仅如此if判断,当前 acquire() 方法执行完毕
- 当前锁被其它线程获取 tryAcquire(arg) 返回false 取反为true,执行&&后的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ,对当前线程进行入列操作存入 AQS 队列中
- FairSync 中的 lock 方法
final void lock() {
acquire(1);
}
- AbstractQueuedSynchronizer 的 acquire() 方法
public final void acquire(int arg) {
//1.调用 FairSync 中的 tryAcquire() 判断当前是否有线程持有锁未释放,设置锁状态
//如果tryAcquire(arg)为true表示没有取反false不进入if语句,说明获取到锁
//2当tryAcquire(arg)为false时取反为true,,说明有线程持有锁,
//执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)当前线程存入AQS队列中
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- FairSync 中的 tryAcquire() 判断是锁是否被其它线程获取,设置锁状态
- tryAcquire() 该方法中判断锁是否被获取实际有三种状态
- 锁被获取,直接返回false
- 锁没被获取,返回true
- 锁被获取,但是获取当前持有锁的线程,判断与当前线程是否是同一个,如果是同一个 state 状态进行累加,锁的重入,返回true
- 当判断锁没有被其它线程持有时, tryAcquire() 方法中会调用 AbstractQueuedSynchronizer 的 hasQueuedPredecessors() 方法,判断当前线程是否需要进行排队,也就是判断 AQS 队列中是否存在其它等待线程,防止出现有线程进行等待,当前线程是在持有锁线程执行完毕释放锁设置 state 为0后—到唤醒队列中等待线程之前,插入进来的
- 当hasQueuedPredecessors() 判断队列中没有其它等待线程后,执行AbstractQueuedSynchronizer 中的 compareAndSetState(0, acquires),通过 Unsafe 自旋原子操作修改 state 状态为已锁
protected final boolean tryAcquire(int acquires) {
//1.获取当前线程
final Thread current = Thread.currentThread();
//2.获取当前状态 state
int c = getState();
//3.如果状态为"0"表示锁没有被获取
if (c == 0) {
//4.hasQueuedPredecessors()判断队AQS 队列中是否存在其它等待线程,
//false 为没有取反为true 执行 compareAndSetState(0, acquires)
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//4.表示当前有线程已经获取到锁,getExclusiveOwnerThread() 获取持有锁的线程,
//判断获取到锁的线程与当前线程是否是同一个线程
else if (current == getExclusiveOwnerThread()) {
//5.如果是同一个线程,锁状态进行累计
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
//返回true
return true;
}
//6.如果已有线程持有锁,并且与当前线程不是同一个,则返回false
return false;
}
}
- AbstractQueuedSynchronizer 中的 hasQueuedPredecessors() 判断 AQS队列中是否有其它等待线程
public final boolean hasQueuedPredecessors() {
Node t = tail; //队列中保存的最后一个阻塞线程
Node h = head; //队列中保存的第一个阻塞线程
Node s;
//1.h != t: 判断AQS 队列中保存第一个阻塞线程与最后一个阻塞线程是否不相等
// 没有阻塞等待线程情况: h与t都为null,相等返回false 不需要排队
//2.(s= h.next )== null : 为false,h头元素调用next为空,表示当前只有一个元素
//3.s.thread != Thread.currentThread() : 获取h头元素线程判断是否是当前线程
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
- AbstractQueuedSynchronizer 中的 compareAndSetState() 基于Unsafe 原子修改 state 状态,加锁成功
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
调用 tryAcquire() 判断锁被其它线程获取后当前线程的后续操作
- 在 FairSync 调用 lock() 方法进行加锁时,该方法中会调用 AbstractQueuedSynchronizer 父类的 acquire() 方法
- acquire() 方法中首先会执行 tryAcquire(arg) 判断锁是否被其它线程获取,返回true锁未被其它线程获取,取反为 false,当前 acquire() 方法执行完毕
- 当通过 tryAcquire(arg) 方法判断锁已被其它线程获取,执行 addWaiter(Node.EXCLUSIVE),当前线程存入 AQS 队列中
- acquireQueued() 当前线程进行等待
public final void acquire(int arg) {
//1.tryAcquire(arg) 判断锁是否被其它线程获取
//2.acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 当前线程入列操作
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- AbstractQueuedSynchronizer 中的 addWaiter() 方法,创建 Node,将当前队列存入 Node,将Node存入 AQS 队列中,只有当前一个需要阻塞的线程,与AQS中已经存在阻塞线程的两种情况
private Node addWaiter(Node mode) {
//1.创建一个 Node 节点,将当前线程存入 Node 中,Node 是 AbstractQueuedSynchronizer
//内部类维护了一个链表,AQS 底层就是通过 Node 存储数据的
Node node = new Node(Thread.currentThread(), mode);
//2.获取 AQS 队列中最后一个节点
Node pred = tail;
//3.判断AQS中是否已存在阻塞队列,存在pred不为null,AQS中有tail尾节点,进入if 维护链表关系
if (pred != null) {
//如果 AQS 中已经有其它等待的线程,维护链表
//设置存有当前线程的 Node 的上一个节点属性为 AQS 的尾节点
node.prev = pred;
//基于Unsafe原子设置为AQS尾节点为存有当前线程的Node节点
if (compareAndSetTail(pred, node)) {
//7.设置AQS 以前的尾节点
pred.next = node;
return node;
}
}
//如果当前AQS队列中没有其它阻塞等待的线程调用 end() 方法,进行入列
enq(node);
return node;
}
- AbstractQueuedSynchronizer 中的 end() 方法将存有当前线程的Node节点入队,设置为 AQS 的 tail 尾节点,如果当前AQS中为空会将当前 tail 的尾节点的下一个节点也是指向当前节点自身,注意点 AQS 中的 header 头节点,无论在什么时候,一直是一个没有存储任何线程的空Node(不是null,初始化但未赋值),可以看为是正在持有锁的线程
private Node enq(final Node node) {
for (;;) {
//1.获取AQS队列中最后一个元素(最后一个等待的线程,在首次有阻塞线程时为null)
Node t = tail;
//2.判断 AQS 中最后一个元素为空
if (t == null) {
//3.创建一个空的Node节点基于Unsafe原子设置为AQS的 header 头节点
if (compareAndSetHead(new Node()))
//4.并设置AQS 的 tail 尾指向 header 头节点(此时 herder头与tail尾都指向同一个空的Node)
tail = head;
} else {
//5.上面的for循环继续执行,维护链表,设置存有当前线程的Node
//节点的上一个节点属性指向AQS以前的最后一个节点
node.prev = t;
//6.基于Unsafe原子设置当前节点为AQS的尾节点,此时AQS中尾节点才有数据
if (compareAndSetTail(t, node)) {
//7.设置AQS的尾节点Node的下一个节点为当前node
t.next = node;
return t;
}
}
}
}
- AbstractQueuedSynchronizer 中的 acquireQueued()设置线程阻塞,该方法中的细节
- 获取AQS中的header节点(可以理解为获取正在执行的线程),判断是否是当前Node的上一个节点,如果当前Node的上一个节点是AQS的header头,说明AQS中没有其它阻塞等待的线程
- tryAcquire(arg) 尝试获取锁,防止正在持有锁的线程在此时执行完毕,释放锁,减少不必要的线程睡眠与唤醒的步骤
- shouldParkAfterFailedAcquire(p, node) 获取存有当前线程的Node的上一个Node中线程的 waitStatus 状态,0改为-1
- parkAndCheckInterrupt() 使用Unsafe的park()方法设置指定线程阻塞
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//通过此处进行中断设置
boolean interrupted = false;
for (;;) {
//1.获取当前Node节点的上一个节点
final Node p = node.predecessor();
//2.p == head : 判断 AQS 的头节点与当前节点的上一个节点是否是同一个
//3.tryAcquire(arg):再次自旋尝试获取锁
if (p == head && tryAcquire(arg)) {
//获取锁成功,或当前节点为头节点,当前线程出列,设置为AQS的header头节点
//并将header头Node中保存的线程为null,prev上一个节点为null,表示出列,
setHead(node);
//设置p的下一个节点为null
p.next = null; // help GC
failed = false;
return interrupted;
}
//4.当前阻塞线程在第2,3步未获取到锁
//5.shouldParkAfterFailedAcquire(p, node) 修改当前节点的上一个节点的 waitStatus为-1
//初始化时为0,首次执行将0修改为-1,返回false不进入当前if,当前的for循环继续执行
//判断waitStatus在上次更给为了-1,返回true,执行执行 parkAndCheckInterrupt()
//6.parkAndCheckInterrupt()方法内部使用Unsafe.park(this) 设置当前线程进入阻塞
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//设置 waitStatus 状态变为-1
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//1.传递的pred节点为当前Node的上一个节点,获取上一个节点的waitStatus状态,初始化时为0
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//基于Unsafe 原子修改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//LockSupport是Unsafe包装类,通过Unsafe设置指定线程阻塞
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
ReentrantLock 中 lockInterruptibly() 获取一个可中断锁
- lockInterruptibly() 直接调用 sync 抽象父类AbstractQueuedSynchronizer 的 acquireInterruptibly()
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
- AbstractQueuedSynchronizer 的 acquireInterruptibly()
public final void acquireInterruptibly(int arg) throws InterruptedException {
//1.防止当前线程还未执行到加锁步骤,就对锁进行了打断
if (Thread.interrupted())
throw new InterruptedException();
//2.tryAcquire(arg) 尝试获取锁,与前面的逻辑一样,获取失败进入if循环,进行排队
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
//1.入列,将当前线程存入Node中,存入AQS
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
//2.前面步骤与获取普通锁时的步骤相同,区别在于此处
//在上面shouldParkAfterFailedAcquire(p, node) 修改当前节点的上一个节点的 waitStatus为-1
//parkAndCheckInterrupt()设置当前线程进入阻塞状态完成后进入if,执行的是 interrupted = true;
//变为了抛出异常,抛出异常后,就不会继续等待当前线程继续执行,所以在获取可中断锁时需要拦截
//InterruptedException异常在catch中编写中断后的处理
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
//将 interrupted = true; 变为了抛出异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
二. ReentrantReadWriteLock
ReentrantReadWriteLock 可以看为读读共享,读写,写读排它,当线程执行被ReentrantReadWriteLock 修饰的代码时,首先获取的是读锁,当有读操作改为写操作时会升级为写锁,当其他线程如果要做写的操作时当前线程也会升级为写锁,读锁时允许其他线程执行做读的操作,写锁时则不允许
/*如果有一个线程已经占用了读锁,则此时其他线程
如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请
写锁或者读锁,则申请的线程会一直等待释放写锁*/
public class Test {
//创建读取锁对象(当同步时,如果如果读取操作线程也会等待,影响效率,解决
//这个问题使用读取锁)
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
//匿名内部类方式创建线程类并运行
//线程类的run方法找那个调用test类的get方法
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public void get(Thread thread) {
//获取读取锁
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} finally {
//释放读取锁
rwl.readLock().unlock();
}
}
//写方法获取写锁: rwl.writeLock().lock()
}
- 示例代码
//ReentrantLock 默认非公平锁
private Lock lock = new ReentrantLock();
//构造器传递true表示当前声明的锁为公平锁
private Lock fairLock = new ReentrantLock(true);
三. 解读源码知道的问题
公平/非公平锁
- 基础层面,如果使用非公平锁,高并发下,可能会出现有的线程抢到锁的频率会比较高,有的线程可能自始至终都没抢到过线程,饿死,而公平锁情况下,多个线程抢到锁的概率是大致相等的,防止出现锁饥饿现象
- 公平是按照请求锁的等待时间,也就是FIFO,否则被视为不公平
- 公平锁获取流程
- 获取锁时判断调用 tryAcquire(arg),未被其它线程获取返回true,
- 如果锁已经被其它线程获取了会调用 acquireQueued(addWaiter(Node.EXCLUSIVE), arg),当前线程入列操作
- 最终会创建一个节点对象调用addWaiter() 存入aqs队列中
- 在执行入列操作时会执行end() 方法将存有当前线程的Node节点入队,设置为 AQS 的 tail 尾节点,如果当前AQS中为空会将当前 tail 的尾节点的下一个节点也是指向当前节点自身
- 注意点在获取锁时调用 tryAcquire(arg)判断锁已经被其它线程获取,会继续判断持有锁的线程与当前线程是否是同一个,如果是同一个 state 状态进行累加,锁的重入,返回true
- 另外一种情况: 当判断锁未被其它线程获取时 tryAcquire() 方法中会调用 AbstractQueuedSynchronizer 的 hasQueuedPredecessors() 方法,判断当前线程是否需要进行排队,也就是判断 AQS 队列中是否存在其它等待线程,防止出现有线程进行等待,当前线程是在持有锁线程执行完毕释放锁设置 state 为0后—到唤醒队列中等待线程之前,插入进来的,当hasQueuedPredecessors() 判断队列中没有其它等待线程后,执行AbstractQueuedSynchronizer 中的 compareAndSetState(0, acquires),通过 Unsafe 自旋原子操作修改 state 状态为已锁
- 为什么会有公平锁与非公平锁的设计?
- 使用公平锁会出现什么问题: Lock基于AQS实现, 公平锁比非公平锁多了几个步骤,需要判断是否有aqs中是否有等待线程,并且在使用非公平锁时,当前线程执行完毕不需要判断队列中是否有前驱等待线程,就可能出现当前线程再去获取锁的几率增大,减少线程切换带来的开销
- 公平锁与非公平锁分别适用于什么场景:如果侧重吞吐量性能使用非公平,