1.Demo
1.1 使用场景
先上一个使用场景:
多个线程之间按照顺序调用,实现ABC三个线程启动,要求如下:
- AA打印5次,BB打印10次,CC打印15次
- 紧接着,AA打印5次,BB打印10次,CC打印15次
- …来10轮
1.2 代码实现
//共享资源类
class ShareResource {
//A 1 B 2 C 3
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5() {
lock.lock();
try {
//1.判断
while (number != 1) {
c1.await();
}
//2.干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//通知
number = 2;
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
//1.判断
while (number != 2) {
c2.await();
}
//2.干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//通知
number = 3;
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
//1.判断
while (number != 3) {
c3.await();
}
//2.干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//通知
number = 1;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class SyncAndReentrantLockDemo {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print5();
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print10();
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print15();
}
}, "CC").start();
}
}
1.3 代码说明
场景关键是要求三个线程是按照ABC的顺序执行,循环10轮,不能乱。
demo中总共有3个线程,A B C,分别调用print5(),print10(),print15()三个方法;
在三个方法中,分别持有3个condition对象,都是先调用wait()方法,等其他线程的signal;然后干完活在使用signal()通知别人抢锁干活。
注意每个方法中都判断了number的数值,number初始值是1,所以AA线程在调用print5()方法时,不会进入wait()方法;相反,其他两个线程要么在阻塞的等待lock锁,要么会进入wait()等待通知抢锁。
2.Condtion原理分析
Condtion是个接口,所有方法的实现都在实现类中实现,好在实现类并不多,只有如下两个:
其实就是AQS类中的ConditionObject内部类(AQS:AbstractQueuedSynchronizer,此类是比较重要的基础方法类,感兴趣可以单独了解,本文只分析Condtion相关的方法,不单独分析AQS中的其他方法);
ConditionObject类中有连个属性:firstWaiter和lastWaiter,说明ConditionObject也是维护了一个condition队列,注意和AQS队列不是同一个队列。 下文中使用condition队列和AQS队列进行区分。
下面看wait方法源码。
2.1 await()方法
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//初始化一个等待节点,加入等待队列尾部
Node node = addConditionWaiter();
//释放AQS同步队列中的节点,就是释放锁,因为调用await肯定是已经获取到锁的
int savedState = fullyRelease(node);
int interruptMode = 0;
//isOnSyncQueue(node):检查node节点是否在AQS的队列中
//如果不在,说明还没有被唤醒,没有资格竞争锁,进入沉睡状态
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//如果已经在AQS同步队列中,acquireQueued方法则进行抢锁
//acquireQueued中如果抢不到,还会再次进入睡眠状态,等待下一次通知抢锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
步骤总结:
1.初始化一个等待节点,加入condition队列尾部,具体看下面addConditionWaiter()源码注释;
2.释放AQS同步队列中的节点,因为之前它肯定是已经拿到锁的;现在都到等待状态了,需要释放之前拿到的锁
3. while (!isOnSyncQueue(node)) 会判断当前线程是否又出现在了AQS队列中,如果没有出现,则继续睡眠等待,如果出现在等待队列中(别的线程调用了signal方法),则进行抢锁;如果抢不到,则继续睡眠。
//初始化一个等待节点,加入等待队列尾部,并返回此节点
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
//判断当前尾部节点的状态,如果不是Node.CONDITION,说明等待被取消了,那么就从队列中删除尾部节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
2.2 signal()方法
继续看通知方法:
public final void signal() {
//能走到这个方法,说明应该是拿到独占锁的,不然就抛异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
//拿到condition等待队列中的头节点,不是空的话,说明有人在等,通知此人
if (first != null)
doSignal(first);
}
//通知队列中的第一个人
private void doSignal(Node first) {
do {
//让等待队列中的第二个节点变成头节点
//如果第二个节点是null,说明没有尾节点了,赋值null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
通知的关键方法是transferForSignal(first),继续看此方法:
final boolean transferForSignal(Node node) {
//将此node的wait状态设置为由Node.CONDITION设置为0,
//如果失败,说明node的状态不是 Node.CONDITION,可能已经被取消了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
//加入AQS的等待队列中
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//唤醒当前节点,正常情况下不会走到这
LockSupport.unpark(node.thread);
return true;
}
步骤总结:通知其实比较简单,分为两步,
1.首先获取到condition队列中的第一个节点,通知此节点
2.通知方法具体动作就是将此节点迁移到AQS队列中去抢锁
以上就是Condition接口的两个核心方法的原理,
注意这里有个比较绕的逻辑关系,lock和aqs和condition这3个对象,
我们new了3个condition对象,那么肯定有3个condition队列,但是,condition是AQS的内部非静态类,是否也存在3个AQS队列?实际通过效果来看,这里只有1个AQS实例对象,否则3个conditon对象将没有任何联系,也就无法进行联动相互通知了。
为什么只有1个AQS对象?我们明明new了3次啊?这里涉及到一个知识点:
看看ReentrantLock的newCondition方法:
public Condition newCondition() {
return sync.newCondition();
}
//sync对象的newCondition方法
final ConditionObject newCondition() {
return new ConditionObject();
}
最终使用的是Sync的newCondition,而Sync继承了AQS,因此相当于是AQS对象的newCondition,此时new出来的3个condition对象,相当于AQS对象的3个内部属性 ,而sync对象又是lock对象的内部属性,所以3个condition对象也就是lock对象的3个内部属性。也就是说,condition对象在进行await()和signal()时,实际上操作的是lock对象内部的sync属性对象中的aqs属性。
最后画一个图: