系列文章索引
并发系列:线程锁事
新系列: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虚拟机相关的源码,因此需要一定的时间去进行积累和沉淀,再做相关的输出
今天的文章就先到这里了,我是释然,我们下篇文章,再见!