J.U.C
Java.util.concurrent(java并发应用包) 是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器、并发集合等等。并发包的作者是大名鼎鼎的 Doug Lea。
ReentrantLock
Lock在J.U.C中是最核心的组件,J.U.C 包中的所有组件绝大部分的组件都有用到了 Lock。在 Lock 接口出现之前,Java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来解决。ReentrantLock的诞生并不是用来取代synchronized的,而是应该在synchronized无法满足的场景下再使用。
1、重入锁概念及基本方法
重入的概念是指在同一个线程内部,这种锁是可以反复进入的。一个线程可多次获取锁,但同时也要释放相同的次数,否则该线程将持续拥有锁,其他线程将无法进入临界区。
lock.lock();
lock.lock();
try{
// do something
}finally{
lock.unlock();
lock.unlock();
}
方法签名 | 方法描述 |
---|---|
void lock(); | 获取锁(如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放) |
void lockInterruptibly(); | 和lock()方法相似, 获取锁,但阻塞的线程可中断,抛出java.lang.InterruptedException 异常 |
boolean tryLock() | 非阻塞获取锁;尝试获取锁,不等待;如果成功返回 true |
boolean tryLock(long timeout, TimeUnit timeUnit) | 在一定时间内尝试获取锁(过时不候) |
void unlock() | 释放锁 |
//当前线程T1 tryLock() 成功后就获取到了锁。此时T2也tryLock() 返回false,那么T2就会执行到else,如果没有else 就会直接结束incr方法。
//同理tryLock(long time, TimeUnit unit) 差不多,只是获取不到锁就会进入到TIMED_WAITING状态,持续时间为指定时间。
public class ReentrantLockDemo2 {
public static Integer count = 0;
static ReentrantLock lock = new ReentrantLock();
public static void incr() throws InterruptedException {
try {
if (lock.tryLock(10, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"获取到锁");
count++;
Thread.sleep(5000);
}else {
System.out.println(Thread.currentThread().getName()+"没有获取到锁,没有执行+1 操作就结束了结束incr方法");
}
}catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {//查询当前线程是否保持此锁定
lock.unlock();
System.out.println(Thread.currentThread().getName() + "线程释放锁");
}
System.out.println(Thread.currentThread().getName() + "线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=============================================");
new Thread(()->{
try {
incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
Thread.sleep(1000);
new Thread(()->{
try {
System.out.println("T2 start..");
incr();
System.out.println("T2 end..");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
Thread.sleep(10000);
System.out.println(count);
}
}
//lockInterruptibly()
public class ReentrantLockDemo3 implements Runnable{
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lockInterruptibly();//当一个线程通过 lockInterruptibly()方法获取锁时,如果该线程处于等待阻塞状态,则该线程可以响应中断
System.out.println(Thread.currentThread().getName() + " running");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " finished");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " interrupted");
}finally {
if (lock.isHeldByCurrentThread()) {//查询当前线程是否保持此锁定
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
}
public static void main(String[] args) {
ReentrantLockDemo3 reentrantLockDemo = new ReentrantLockDemo3();
Thread thread01 = new Thread(reentrantLockDemo, "thread01");
Thread thread02 = new Thread(reentrantLockDemo, "thread02");
thread01.start();
thread02.start();
thread02.interrupt();
}
}
1.1 中断响应
对于synchronized来说,一个线程要么获取到锁开始执行,要么继续等待。但是对于重入锁来说,提供了更灵活的一种机制,那就是在等待锁的过程中,可以取消对锁的请求,这样可以有效避免死锁的可能。
1.2 锁申请等待时间
中断响应是一种通过外部通知中断对锁的请求,从而避免死锁的一种机制。除此之外,还有一种机制,那就是等待限时。
1.3 公平锁、非公平锁
重入锁默认是非公平的。可以通过构造函数实现公平锁。如果是非公平锁,在并发场景下,系统会随机从等待队列中挑选一个线程。如果是公平锁,系统会维护一个有序队列,会按照进入队列的次序有序执行,因此公平锁虽然避免了饥饿现象,但是会需要更高的成本来维护这个有序队列。
//无参构造函数,默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();//通过内置的sync对象加锁
}
//传入布尔值,true-公平锁 false-非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.重入锁的实现原理
ReentrantLock定义了内部类Sync,Sync继承自AbstractQueuedSynchronizer(简称AQS),是一个同步等待队列,本质上是一个带有头尾指针的双向链表。sync并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。Sync 有两个具体的实现类,分别是:
-
NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
-
FailSync: 表示所有线程严格按照 FIFO 来获取锁
FairSync和NonfairSync都继承自Sync,他们的继承关系如下图,都是最终继承自AbstractQueuedSynchronizer:
AQS依赖先进先出队列实现了阻塞锁和相关的同步器。AQS内部有一个volatile类型的state属性,实际上多线程对锁的竞争体现在对 state 值写入的竞争。一旦 state 从 0 变为 1,代表有线程已经竞争到锁,那么其它线程则进入等待队列。等待队列是一个链表结构的 FIFO 队列,这能够确保公平锁的实现。同一线程多次获取锁时,如果之前该线程已经持有锁,那么对 state 再次加 1。释放锁时,则会对 state-1。直到减为 0,才意味着此线程真正释放了锁。
AQS 的功能分为两种:独占和共享锁,每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁;共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock。
private volatile int state;
重入锁的加锁和解锁过程主要有AQS完成,AQS维护了一个双向链表,每个节点Node存储一个线程及线程的状态,Head节点代表正在持有锁的线程。
当线程获取锁失败之后,就通过addWaiter加入到同步队列中(加入到尾部),自旋判断自己是否是链表的头节点,如果是头节点,就不断参试获取资源,获取成功后则退出同步队列。
2.1 公平锁加锁过程
sync.lock时调用AQS的acquire方法,这是一种模板设计模式,即AQS中定义了整体的处理流程,但是具体的实现细节会根据锁类型的不同,放到子类方法中执行。
public void lock() {
sync.lock();
}
//以公平锁为例
final void lock() {
acquire(1);
}
//AQS -- FairSync并没有重写acquire方法代码,因此调用的是AQS中的
public final void acquire(int arg) {
//首先尝试调用一次tryAcquire方法,1)若返回true,则立刻返回
//2)若为false,则会先调用addWaiter()方法,将线程包装成一个Node,加入到等待队列;
//再调用acquireQueued()尝试排队获取锁,如果成功后发现自己被中断过,则返回true,否则false,立刻返回
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire()方法:
-
通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
-
如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
-
acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁
tryAcquire(int acquires)在FairSync和NonfairSync中有不同的实现。
//ReentrantLock
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//表明目前锁空闲,没有线程占用
if (!hasQueuedPredecessors() && //1.判断队列前是否已有线程在排队,若没有,进行第二个判断。
compareAndSetState(0, acquires)) { //2.CAS,比较当前状态是否为0,若是,则修改为acquires
setExclusiveOwnerThread(current);//3.修改当前锁属于的线程
return true;
}
}
//若锁被占用,判断拥有锁的线程是否为当前线程(可重入)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;//若是,则将state + 1(acquires)
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置state
return true;
}
return false;
}
addWaiter(Node.EXCLUSIVE):
//AQS
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//把当前线程封装为 Node
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;//tail 是 AQS 中表示同步队列队尾的属性,默认是 null
if (pred != null) {//tail 不为空的情况下,说明队列中存在节点
node.prev = pred;//把当前线程的 Node 的 prev 指向 tail
if (compareAndSetTail(pred, node)) {//通过 cas 把 node加入到 AQS 队列,也就是设置为 tail
pred.next = node;//设置成功以后,把原 tail 节点的 next指向当前 node
return node;
}
}
enq(node);//通过自旋操作把当前节点加入到队列中
return node;
}
//AQS
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//自旋
final Node p = node.predecessor();//获取当前节点的 prev 节点
if (p == head && tryAcquire(arg)) {//如果是 head 节点,说明有资格去争抢锁“如果是第一个排队的,那么就再去尝试获取锁”
setHead(node);//获取锁成功,也就是ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权限
p.next = null; // help GC -- 把原 head 节点从链表中移除
failed = false;
return interrupted;
}
//ThreadA 可能还没释放锁,使得ThreadB 在执行 tryAcquire 时会返回 false
//检查是否应该被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
}
} finally {
if (failed)
cancelAcquire(node);//取消获得锁的操作
}
}
假设有三个线程来争抢锁,AQS中的链表结构图如下:
2.2公平锁与非公平锁的区别:
锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。 在上面分析的例子来说,只要CAS 设置同步状态成功,则表示当前线程获取了锁,而公平锁则不一样,差异点有两个:
首先是lock中的实现
-
非公平锁和公平锁最大的区别在于,在非公平锁中抢占锁的逻辑是,不管有没有线程排队,先 cas 去抢占一下
-
CAS 成功,就表示成功获得了锁
-
CAS 失败,调用 acquire(1)走锁竞争逻辑
//非公平锁
final void lock() {
if (compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());
} else{
acquire(1);
}
}
//公平锁
final void lock() {
acquire(1);
}
compareAndSetState()
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this,stateOffset, expect, update);
}
通过CAS乐观锁的方式来做比较并替换,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false。这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到 state 这个属性的意义。
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示
当 state=0 时,表示无锁状态
当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0 其他线程才有资格获得锁 。
Unsafe 类:Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Hadoop、Kafka 等;Unsafe 可认为是 Java 中留下的后门,提供了一些层次操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障,而 CAS 就是 Unsafe 类中提供的一个原子操作,第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值。整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false;
tryAcquire()实现:
//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//公平锁多了一个!hasQueuedPredecessors() 这个方法
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
...
}
//非公平锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
...
}
-
非公平锁在获取锁的时候,会先通过 CAS 进行抢占,而公平锁则不会
-
公平锁在tryAcquire方法中多了hasQueuedPredecessors()方法,也就是加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁
2.3ReentrantLock.unlock 释放锁
在 unlock 中,会调用 release 方法来释放锁
public void unlock() {
sync.release(1);
}
//AQS
public final boolean release(int arg) {
if (tryRelease(arg)) { //释放锁成功
Node h = head; //得到 AQS 中 head 节点
if (h != null && h.waitStatus != 0)//如果 head 节点不为空并且状态!=0.调用
unparkSuccessor(h);//唤醒后续节点
return true;
}
return false;
}
protected final boolean tryRelease(int releases)
{
int c = getState() - releases; //减1(releases)
if (Thread.currentThread() != getExclusiveOwnerThread()) //判断当前线程是否为拥有锁的线程
throw new IllegalMonitorStateException();
boolean free = false;//是否释放成功
if (c == 0) {//state值减为0,表示锁释放完了(可重入)
free = true;
setExclusiveOwnerThread(null);//设置当前锁空闲,不被线程拥有
}
setState(c);//设置state
return free;
}
tryRelease()是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁(独占)的 Owner 设置为 null,以使得其它的线程有机会进行执行。
在排它(独占)锁中,加锁的时候状态会增加 1(可以修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。
unparkSuccessor(h);//唤醒后续节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;//获得 head 节点的状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点状态为 0
Node s = node.next;//得到 head 节点的下一个节点
if (s == null || s.waitStatus > 0) {
//如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
//通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev){
if (t.waitStatus <= 0)
s = t;
}
if (s != null) //next 节点不为空,直接唤醒这个线程即可
LockSupport.unpark(s.thread);
}
}
通过锁的释放,原本的结构就发生了一些变化。head 节点的 waitStatus 变成了 0,ThreadB 被唤醒。