1. 问题描述
如何让多个线程交替执行是Java并发中的常见问题。现在就让我们通过一个具体例子来解决这个问题。
场景:现在假设有30张票需要出售。现在有三个售票处对该票进行售卖,现在要求着三个售票处以此对票进行出售。
2. 代码
2.1 资源类定义
首先,我们需要先定义资源类及其行为。我们当前问题中的资源很显然就是要出售的票,行为就是三个售票处售票的动作。资源类定义如下:
class Ticket {
private int num = 30;
private final Lock lock = new ReentrantLock();
private int flag = 1;
private final Condition c1 = lock.newCondition();
private final Condition c2 = lock.newCondition();
private final Condition c3 = lock.newCondition();
public void sail1() throws InterruptedException {
lock.lock();
try {
while(flag != 1) {
c1.await();
}
if(num > 0) {
num--;
}
System.out.println(Thread.currentThread().getName() + "::" + num);
flag = 2;
c2.signal();
}finally {
lock.unlock();
}
}
public void sail2() throws InterruptedException {
lock.lock();
try {
while(flag != 2) {
c2.await();
}
if(num > 0) {
num--;
}
System.out.println(Thread.currentThread().getName() + "::" + num);
flag = 3;
c3.signal();
}finally {
lock.unlock();
}
}
public void sail3() throws InterruptedException {
lock.lock();
try {
while(flag != 3) {
c3.await();
}
if(num > 0) {
num--;
}
System.out.println(Thread.currentThread().getName() + "::" + num);
flag = 1;
c1.signal();
}finally {
lock.unlock();
}
}
}
- 变量
num
即代表当前票的数量 lock
是为了保证三个线程同步执行的锁flag
有三种取值1,2,3,分别代表三个售票处,这是为了与c1
,c2
和c3
进行配合,完成线程的交替执行sail1()
是第一个售票处进行售票的行为函数。其思路如下:- 首先就是要获取锁,保证同步操作
- 如果当前
flag
不等于1,就说明此时不应该第一个售票处进行售票,那么就释放当前锁,进行等待 - 如果当前
flag
等于1,说明此时第一个售票处应该执行后续的售票操作,那么num--
,完成售票操作 - 将票售出之后,要通知接下来的第二个售票处进行售票,那么就将
flag
赋值为2,且唤醒该线程 - 完成所有操作后,将锁释放
- 与第一个售票处线程类似,第二号在完成操作后,要将
flag
赋值为3,唤醒第三个售票处线程 - 第三号售票处要唤醒第一号售票处线程,完成交替执行
注意 :
await()
函数一般都要放在循环中,这是因为lock
与synchronized
一样,都是在哪里等待就在哪里被唤醒。如果放在if
中,那么被唤醒之后会紧接着执行后续代码,不会判断此时flag是否符合条件,破坏交替执行。- 在目前场景中,即使放在
if
中也是可以的。因为此时每次都只会唤醒一个目标线程。如果第一号线程执行后,唤醒第二号和第三号线程。那么,此时如果是if
,就无法保证接下来是第二号执行,第三号线程也可能会获得锁后跳过判断flag
步骤执行进行num--
。 - 释放锁的过程要放在
finally
中。这是因为lock
与synchronized
不同,不会自动完成锁的释放过程。如果代码中出现异常,没有完成释放,那么就会出现死锁。因此要在finally
中加入unlock()
,保证锁被顺利释放。
2.2 主函数
资源类定义完成后,我们就可以创建三个不同的类对资源类进行交替处理,代码如下:
public class Main {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
ticket.sail1();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "AA").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
ticket.sail2();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "BB").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
ticket.sail3();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "CC").start();
}
}
创建了三个线程AA,BB,CC分别代表第一、第二、第三号售票处。
2.3 结果
AA::29
BB::28
CC::27
AA::26
BB::25
CC::24
AA::23
BB::22
CC::21
AA::20
BB::19
CC::18
AA::17
BB::16
CC::15
AA::14
BB::13
CC::12
AA::11
BB::10
CC::9
AA::8
BB::7
CC::6
AA::5
BB::4
CC::3
AA::2
BB::1
CC::0
可以看到三个线程确实是交替执行,且num也没有出现错误。