上一篇讲解了Condition接口各个API的功能,本文继续对ReentrantLock+Condition的具体使用进行深入分析。
实现2个线程依次输出123456…
在系列文章的第二篇中,分析了采用synchronized与wait+notify实现一个经典的题目:如何控制2个线程依次输出1 2 3 4 5 6…?既然ReentrantLock可以代替synchronized,Condition可以代替wait+notify,可以实现更精细的并发协同控制,那么我们就来用ReentrantLock+Condition来实现这一个经典的题目。
ReentrantLock+Condition方案
我们回顾下这个题目的思路:
步骤1:创建2个线程t1,t2。
步骤2:t1运行输出1,t1停止输出(等待t2输出完,进入等待状态)。
步骤3:t2运行输出2,t2停止输出(等待t1输出完,进入等待状态)。
步骤4:循环执行步骤2和步骤3,即t1和t2依次执行”运行<->等待“动作。
这其中有两个等待,一个是“t1等待t2”,一个是“t2等待t1”,结合前面所学的Condition可以创建多个等待条件变量,可以创建两个Condition条件用来表示“t1等待t2”和“t2等待t1”。Condition提供了让线程进入等待状态的await()方法和唤醒其他线程的signal()方法,可以用来代替Object来的wait()方法和notify()方法。基于上面分析,我们对之前的代码改造如下:
public class ReentrantLockCollaboration {
//输出数字
private static int num = 1;
//公共变量,线程执行标识
private static boolean thread1Run = true;
//锁对象
private static final ReentrantLock lockObject = new ReentrantLock();
// 线程1运行条件
private static final Condition thread1RunCondition = lockObject.newCondition();
// 线程2运行条件
private static final Condition thread2RunCondition = lockObject.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (num < 10) {
//获取锁对象
lockObject.lock();
try {
//判断是否轮到自己执行
while (!thread1Run) {
try {
//没到就进入等待
thread1RunCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
thread1Run = false;
//执行完输出,通知t2执行
thread2RunCondition.signal();
} finally {
lockObject.unlock();
}
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (num < 10) {
//获取锁对象
lockObject.lock();
try {
//判断是否轮到自己执行
while (thread1Run) {
//没到就进入等待
try {
thread2RunCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
thread1Run = true;
//执行完输出,通知t2执行
thread1RunCondition.signal();
} finally {
lockObject.unlock();
}
}
}
}, "t2");
t1.start();
t2.start();
}
}
运行结果:
t1 output 1
t2 output 2
t1 output 3
t2 output 4
t1 output 5
t2 output 6
t1 output 7
t2 output 8
t1 output 9
t2 output 10
结果符合我们的预期。
实现3个线程依次输出123456…
上面的例子中只有2个线程,一个线程输出完成后就唤醒另外的一个线程,现在我们对题目升级下:用3个线程依次输出1 2 3 4 5 6…那该如何实现?
按之前的方法我们对题目进行步骤拆解分析:
步骤1:创建3个线程t1,t2,t3。
步骤2:t1运行输出1,t1停止输出,进入等待状态(等待t2,t3输出完成)。
步骤3:t2运行输出2,t2停止输出,进入等待状态(等待t3,t1输出完成)。
步骤4:t3运行输出3,t3停止输出,进入等待状态(等待t1,t2输出完成)。
步骤5:循环执行步骤2、步骤3、步骤4,整个过程只有一个线程处于输出状态,另外两个线程处于等待状态。
先思考下,如果用synchronized与wait+notify该怎么实现呢?在只有2个线程的情况下,通过公共的变量用来标识是否轮到自己执行,2个线程内部通过判断公共变量的值是否轮到自己执行,不是t1执行就是t2执行。但是现在变成3个线程了,用一个公共变量标识是否轮到自己执行明显不可行了,因为t1执行修改公共变量后,t2和t3都读取到公共变量轮到自己执行,无法控制是t2先执行还是t3先执行。
那么通过什么方式标识是否轮到自己执行呢?也就是用什么来做无限循环条件判断?找出无限循环判断条件是解决这个题目的核心点。这个题目还有哪些特征?根据对题目的步骤分析,我们推导出程序的运行结果:
t1 output 1
t2 output 2
t3 output 3
t1 output 4
t2 output 5
t3 output 6
t1 output 7
t2 output 8
t3 output 9
观察下上面的预期结果,我们发现线程的序号和输出数字直接存在一定的关系:输出数字除以3的余数和线程序号除以3的余数相等。
1除以3的余数是1;
2除以3的余数是2;
3除以3的余数是0;
4除以3的余数是1;
5除以3的余数是2;
6除以3的余数是0;
…
所以我们可以通过这个关系来判断是否轮到当前线程执行,输出数字除以3的余数和线程序号除以3的余数相等说明轮到当前线程执行。
synchronized与wait+notify方案
基于上述分析,我们先用synchronized与wait+notify的方式实现,代码如下:
public class ConcurrentCollaboration {
//输出数字
private static int num = 1;
//锁对象
private static final Object lockObject = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(num < 10) {
//获取锁对象
synchronized (lockObject) {
//判断是否轮到自己执行
while (num%3 != 1%3) {
//没到就进入等待
try {
lockObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
//执行完输出,通知t2,t3执行
lockObject.notifyAll();
}
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while(num < 10) {
//获取锁对象
synchronized (lockObject) {
//判断是否轮到自己执行
while (num%3 != 2%3) {
//没到就进入等待
try {
lockObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
//执行完输出,通知t1,t3执行
lockObject.notifyAll();
}
}
}
},"t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
while(num < 10) {
//获取锁对象
synchronized (lockObject) {
//判断是否轮到自己执行
while (num%3 != 3%3) {
//没到就进入等待
try {
lockObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
//执行完输出,通知t1,t2执行
lockObject.notifyAll();
}
}
}
},"t3");
t1.start();
t2.start();
t3.start();
}
}
我们对创建线程任务代码的方式改造下,用更符合面向方式改造代码:
public class ConcurrentCollaboration {
//输出数字
private static int num = 1;
//锁对象
private static final Object lockObject = new Object();
//输出数字最大值
private static final int max = 10;
public static void main(String[] args) {
Thread t1 = new Thread(new Printer(1));
Thread t2 = new Thread(new Printer(2));
Thread t3 = new Thread(new Printer(3));
t1.start();
t2.start();
t3.start();
}
static class Printer implements Runnable {
private final int threadId;
public Printer(int threadId) {
this.threadId = threadId;
}
@Override
public void run() {
while (num < max) {
synchronized (lockObject) {
//判断是否轮到自己执行
while (num % 3 != threadId % 3) {
try {
//没到就进入等待
lockObject.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (num <= max) {
System.out.println("t" + threadId + " output " + num);
num++;
}
//执行完输出,通知其他线程执行
lockObject.notifyAll();
}
}
}
}
}
运行程序结果符合我们的预期。
ReentrantLock+Condition方案
synchronized与wait+notify的方案中,无法为指定的线程设置单独的等待条件,也无法指定唤醒某个线程。我们可以使用Condition给3个线程设置不同的条件变量,通过条件变量指定某个线程进入等待或者唤醒状态。实现如下:
public class ConcurrentCollaboration {
//锁对象
private static final ReentrantLock lockObject = new ReentrantLock();
// 线程1运行条件
private static final Condition condition1 = lockObject.newCondition();
// 线程2运行条件
private static final Condition condition2 = lockObject.newCondition();
// 线程3运行条件
private static final Condition condition3 = lockObject.newCondition();
//输出数字
private static int num = 1;
//输出数字最大值
private static final int max = 10;
public static void main(String[] args) {
Thread t1 = new Thread(new Printer(1));
Thread t2 = new Thread(new Printer(2));
Thread t3 = new Thread(new Printer(3));
t1.start();
t2.start();
t3.start();
}
static class Printer implements Runnable {
private final int threadId;
public Printer(int threadId) {
this.threadId = threadId;
}
@Override
public void run() {
while (num < max) {
//获取锁
lockObject.lock();
try {
//判断是否轮到自己执行
while (num % 3 != threadId % 3) {
try {
//没到就进入等待
if (threadId == 1) {
condition1.await();
} else if (threadId == 2) {
condition2.await();
} else if (threadId == 3) {
condition3.await();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (num <= max) {
System.out.println("t" + threadId + " output " + num);
num++;
}
//执行完输出,通知其他线程执行
if (threadId == 1) {
condition2.signal();
} else if (threadId == 2) {
condition3.signal();
} else if (threadId == 3) {
condition1.signal();
}
} finally {
//释放锁
lockObject.unlock();
}
}
}
}
}
运行程序结果同样符合我们的预期。通过上面的例子可以看出,通过Condition条件变量可以进行具体到某个线程更细粒度的等待和唤醒控制。
ReentrantLock在jdk中的具体应用
ReentrantLock的典型应用在生产者-消费者协模型中,如jdk中的ArrayBlockingQueue。ArrayBlockingQueue是Java并发包中基于数组实现的有界阻塞队列,其核心设计围绕线程安全和生产者-消费者协作展开。下面我们就来分析ReentrantLock在ArrayBlockingQueue中是如何保证线程安全的。
锁与条件变量的定义
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, Serializable {
private static final long serialVersionUID = -817911632652898426L;
//存储数据的数组
final Object[] items;
//指向下一个待出队元素的数组下标
int takeIndex;
//指向下一个待入队元素的数组下标
int putIndex;
//实际元素数量
int count;
//锁
final ReentrantLock lock;
//非空的条件变量
private final Condition notEmpty;
//非满的条件变量
private final Condition notFull;
transient ArrayBlockingQueue<E>.Itrs itrs;
......
/**
* Creates an {@code ArrayBlockingQueue} with the given (fixed)
* capacity and the specified access policy.
*
* @param capacity the capacity of this queue
* @param fair if {@code true} then queue accesses for threads blocked
* on insertion or removal, are processed in FIFO order;
* if {@code false} the access order is unspecified.
* @throws IllegalArgumentException if {@code capacity < 1}
*/
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
//创建数组对象
this.items = new Object[capacity];
//创建锁
lock = new ReentrantLock(fair);
//创建条件变量
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
}
ArrayBlockingQueue定义了ReentrantLock类型的lock对象,一个Condition类型队列非空的条件变notEmpty,一个Condition类型队列非满的条件变量notFull,这三个对象保证队列数据入队和出队是的线程安全操作。这三个对象在ArrayBlockingQueue的构造函数值进行初始化。下面摘取关键的入队(如put)和出队(如take)操作来分析线程安全控制方法。
put操作
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
//获得锁对象
ReentrantLock lock = this.lock;
//上锁,可中断
lock.lockInterruptibly();
try {
//无限循环判断队列实际元素数量是否等于数组长度
while(this.count == this.items.length) {
//非满条件阻塞生产者,停止插入元素
this.notFull.await();
}
//插入元素
this.enqueue(e);
} finally {
//释放锁
lock.unlock();
}
}
private void enqueue(E e) {
Object[] items = this.items;
//通过待入队元素的数组下标定位插入位置
items[this.putIndex] = e;
if (++this.putIndex == items.length) {
this.putIndex = 0;
}
//队列实际长度加1
++this.count;
//非空条件唤醒消费者线程取数据
this.notEmpty.signal();
}
如代码所示,put方法内部在插入元素前先去获取一把锁,获取过程可以被中断,然后通过无限循环判断队列实际元素数量是否等于数组长度,等于则通过非满条件阻塞生产者,停止插入元素。否则执行插入元素逻辑,通过待入队元素的数组下标定位插入位置,队列实际长度加1,然后通过非空条件唤醒消费者线程。
take操作
public E take() throws InterruptedException {
//获得锁对象
ReentrantLock lock = this.lock;
//上锁,可中断
lock.lockInterruptibly();
Object var2;
try {
//无限循环判断队列实际元素数量是否等于0
while(this.count == 0) {
//非空条件阻塞消费者,停止取数据
this.notEmpty.await();
}
//取数据
var2 = this.dequeue();
} finally {
//解锁
lock.unlock();
}
return var2;
}
private E dequeue() {
Object[] items = this.items;
//通过待出队元素的数组下标
E e = items[this.takeIndex];
items[this.takeIndex] = null;
if (++this.takeIndex == items.length) {
this.takeIndex = 0;
}
//队列长度减1
--this.count;
if (this.itrs != null) {
this.itrs.elementDequeued();
}
//非满条件唤醒生产者线程放入数据
this.notFull.signal();
return e;
}
如代码所示,take方法内部在获取元素前先去获取一把锁,获取过程可以被中断,然后通过无限循环判断队列实际元素数量是否等于0,等于则通过非空条件阻塞消费者,停止取数据。否则执行取出元素逻辑,通过待出队元素的数组下标定位取出元素位置,队列长度减1,然后通非满条件唤醒生产者线程放入数据。
线程安全分析
通过上面的ArrayBlockingQueue的部分关键代码分析,ArrayBlockingQueue内部存在共享资源竞争,底层的数组(items)、计数器(count)和入队出队位置索引(putIndex/takeIndex)是多个线程共享的变量。对这些共享变量操作均需要在加锁后才能操作,确保多线程环境下对队列的各种操作行为的正确性。
其次,一个入队和出队操作均包含多个步骤,比如入队时需要先判断数组长度、再写入数据到指定为止、指向下一个入队元素的数组下标加1,队列实际长度加1。完成这一组步骤必须作为一个原子单元执行,保证要么执行全部完成,要么执行失败,不允许出现一部分执行成功,一部分执行失败的情况,保证了数据的完整性和正确性。
最后,通过锁与多条件变量的控制,队列满了则生产者调用await()方法阻塞,非满则插入数据并调用signal()唤醒消费者取数据。消费者取数据时,队列为空则调用signal()唤醒生产者存入数据。通过这一系列的阻塞-唤醒操作实现多线程之间的高效协作。
总结
本文以多线程依次输出123456…的经典例子来讲解ReentrantLock+Condition的使用,并与采用synchronized与wait+notify实现方案进行对比,体现了ReentrantLock+Condition更精细的并发协同控制优势。最后通过分析ReentrantLock在ArrayBlockingQueue的具体应用,进一步加深了对ReentrantLock+Condition使用的理解。