系列文章索引
并发系列:线程锁事
新系列:Android11系统源码解析
-
Android11源码分析:binder是如何实现跨进程的?(创作中)
-
Android11源码分析:SurfaceFlinger是如何对vsync信号进行分发的?(创作中)
经典系列:Android10系统启动流程
前言
前面我们对并发容器和线程协作工具进行了相关源码分析,今天我们将从使用出发,并继续深入源码,看看ReentraientLock
是如何对锁的使用进行封装和优化的
下面,正文开始
使用ReentraientLock
实现顺序打印
在篇一:为什么CountDownlatch能保证执行顺序?中,我们使用CountdownLatch
实现了顺序打印的需求,并且分析了其原理其实是线程间的通知和唤醒
ReentraientLock
作为锁的封装和实现,同样支持线程间的通知和唤醒,针对到具体的方法为singal()
(通知其他线程获取锁),await()
释放锁并等待
具体实现中会创建一个ReentrantLock
对象,并创建了4个条件变量Condition
-
当condition不满足时,调用
condition.await()
释放锁并等待 -
当线程执行完成后,调用
condition.single()
唤醒对应的线程继续执行
具体代码如下
/**
* 使用Condition条件变量控制打印流程
*/
public void conditionPrint() {
ReentrantLock lock = new ReentrantLock();
Condition aCondition = lock.newCondition();
Condition bCondition = lock.newCondition();
Condition cCondition = lock.newCondition();
Condition dCondition = lock.newCondition();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
printStr("a");
aCondition.signal();
} finally {
lock.unlock();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
aCondition.await();
printStr("b");
bCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
bCondition.await();
printStr("c");
cCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
cCondition.await();
printStr("d");
dCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
t3.start(); t4.start(); t2.start();t1.start();
printStr("conditionPrint-打印开始");
try {
lock.lock();
dCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
printStr("conditionPrint-打印结束");
}
}
ReeentraientLock
详解
到底什么是ReentraientLock
?
故名思意,ReentraientLock
应该是一个可重入锁
可重入锁
的意思是,当一个线程持有一个锁对象,在对其进行解锁后再执行加锁操作,此时可以直接获取到锁对象继续执行
关于多个锁对一把锁的争抢问题,在AbstractQueuedSynchronizer
(后面简称AQS
)中会维护一个线程队列,当通知线程去获取锁时,会从线程队列中取出申请锁的线程,让该线程继续执行
在ReentraientLock
中,会继承AQS
实现自己的Sync
,其中的非公平锁实现NonfairSync
及公平锁实现FairSync
都是Sync
的子类对象
而ReentraientLock
的默认构造会使用NonfairSync
(非公平锁,AQS
的实现类)进行获取和释放的逻辑,同时可以在构造中传入fair
的参数,控制是否使用公平锁,如果传入true
,则会使用FairSync
进行锁的释放
什么是公平锁和非公平锁?
在聊具体的代码前,我们先来讲一下什么叫公平锁
上文也提到了,在AQS
中会维护一个Thread
的双向循环链表,用来第获取锁的线程进行保存
如果所有线程对于加锁和解锁操作都完全按照申请顺序进行存取,那就是公平锁
的实现思路
如果在某种条件下,允许某些线程获取锁的操作进行插队
,那就是非公平锁
的实现思路
在具体的使用中,我们在进行加锁时,会调用lock.lock()
在进行解锁时会调用lock.unlock()
,其内部会调Sync
的lock()
函数及release()
函数
在进行线程等待时会调用await()
,唤醒等待线程会调用signal()
下面,我们就针对具体的实现来分析下
非公平锁NonfairSync
默认构造中,会调用非公平锁的实现,执行lock()
方法, 其中会通过CAS
操作对AQS
中的state
值进行修改
在ReentraientLock
中,state
表示对锁的重入次数,state为0表示没有线程持有锁,state非0表示有线程正在持有锁
具体代码如下
static final class NonfairSync extends Sync { //非公平锁
//...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
如果修改成功(state为0),表示没有线程获取当前的锁,会将当前线程的Thread
对象保存在AQS
的exclusiveOwnerThread
中,表示该线程获取到锁,可以执行相关逻辑
如果修改失败(state非0),表示线程锁已被持有,会调用到AQS
的acquire()
函数,将当前线程添加到请求队列的链表中
其中的acquire(1)
会调用到AQS
中的函数,并调用到了子类(即NonfairSync
)的tryAcquire(1)
函数申请锁的执行权,最终调用到了nonfairTryAcquire()
代码如下
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); //state表示线程重入的次数
if (c == 0) { //表示没有线程持有锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); //将当前线程设置为执行线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //表示当前申请锁的线程和持有锁的线程是同一个线程,执行锁的重入逻辑
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); //更新state值
return true;
}
return false;
}
该函数是可重入锁
逻辑的核心实现:
-
如果
state==0
,表示当前锁没有被持有,将其设置为执行线程并返回true -
如果
state!=0
,且申请锁的线程与当前持有锁的线程一直,则通过state
维护锁的重入次数累加,并返回true -
如果
state!=0
,且申请锁的线程不是当前线程,则申请失败,返回false
如果返回true,表示当前线程申请到锁,继续执行doAcquireInterruptibly()
,接着将Thread
封装为Node节点添加到链表中,设置为头节点
代码如下
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
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;
}
//...
}
} finally {
if (failed)
cancelAcquire(node);
}
}
至此,我们将非公平锁的实现分析完毕了,现在来简单总结一下
小结
非公平锁
的实现中,当前持有锁的线程在锁释放后再获取锁,可以直接获取到锁继续执行,这就是所谓的非公平
实现,也可以叫可重入锁
(针对当前持有锁的线程是可重入的)
Java中的synchronize
和lock
的实现都是可重入的,非公平锁
(或者说可重入锁)的好处在于,
-
节省了线程唤醒的开销,性能上更优
-
当前持有锁的线程在释放后再申请锁时,可以直接获取到锁对象,避免了死锁的发生
公平锁FairSync
针对FairSync
而言,实现了绝对的公平
,不论当前线程是否持有锁,再次获取锁对象时都需要添加到线程的等待队列,按插入顺序去等待被唤醒
其优点在于,保证了绝对的公平
,可以按照线程的启动顺序(先进先出)去顺序执行
缺点在于,不可重入性导致了性能上不如非公平锁
具体代码此处不再展示,感兴趣的小伙伴可以自行查看
加餐:不使用锁如何保证线程安全?
在源码分析的时候,我们发现ReentraientLock
中对线程互斥的维护并没有使用加锁的逻辑,而是使用CAS
(compareAndSwap)对state
值进行操作去实现的
在java并发包中,原子类数据结构也是通过CAS
指令去实现的原子性操作,保证无锁
条件下的并发安全
那AtomicInteger
来举例,其中也是类似与ReentraientLock
中对state
变量的CAS
修改操作去实现的
因此我们可以也可以使用原子类来实现本文中的顺序打印需求
-
创建一个AtomicInteger,并初始化其
value
为4 -
在各线程进行循环判断,当
value
满足条件时执行打印,并将value
值-- -
在
value
倒数到0后,说明4各线程都已经执行完成,在主线程中循环等待条件满足时终止子线程
代码如下
public void atomicPrint() {
AtomicInteger lock = new AtomicInteger(4);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (lock.get() == 4) {
System.out.println("Thread:" + "a" + " lock count:" + lock.get());
System.out.println("a");
lock.decrementAndGet();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (lock.get() == 3) {
System.out.println("Thread:" + "b" + " lock count:" + lock.get());
System.out.println("b");
lock.decrementAndGet();
}
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (lock.get() == 2) {
System.out.println("Thread:" + "c" + " lock count:" + lock.get());
System.out.println("c");
lock.decrementAndGet();
}
}
}
});
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (lock.get() == 1) {
System.out.println("Thread:" + "d" + " lock count:" + lock.get());
System.out.println("d");
lock.decrementAndGet();
}
}
}
});
System.out.println("atomicPrint-执行开始");
t3.start(); t4.start();t2.start();t1.start();
while (!Thread.currentThread().isInterrupted()) {
if (lock.get() == 0) {
t3.interrupt();
t4.interrupt();
t2.interrupt();
t1.interrupt();
System.out.println("atomicPrint-执行结束");
lock.decrementAndGet();
return;
}
}
Thread.currentThread().interrupt();
}
小结
我们在使用原子类维护线程按顺序执行时,需要在各线程中,通过原子类的方法循环判断其是否满足条件,相当与自旋操作,因此在开销上比循环等待要高,一般情况下我们还是应该使用线程协作的方式去实现
当需要对某种状态进行原子性操作时,可以使用原子类的相关实现,比加锁的方式效率更高
最后
本篇文章是「线程琐事」的第三篇,按照计划这是最后一篇了,但并发领域确实博大精深,远不是几篇文章能分析完的,因此可能还会有后续,比如针对java内存模型
进行分析讲解,针对原子性
/有序性
/可见性
进行分析
按照我的学习和分析模式,一定是理论结合源码的,这里必不可少要涉及到jvm
虚拟机相关的源码,因此需要一定的时间去进行积累和沉淀,再做相关的输出
今天的文章就先到这里了,我是释然,我们下篇文章,再见!