1、题目描述
Thread1:1 2 3
Thread2: a b c
最终输出结果:1a2b3c
2、法一:使用park()和unpark()方法
2.1 整体流程解读:
为了保证t1在t2前面执行,所以t2的run方法第一句是阻塞自己,如果t2线程先start,会阻塞,等待t1线程输出数字之后,唤醒t2。实现数字字母交替输出的结果。
2.2 疑问解答:
1. 代码是先t1.start(),后t2.start();为什么还要保证t1在t2前面执行?
线程谁先start()运行是随机的!并不一定和代码编写顺序一致。
2. 如果t1先执行,先调用unpark(t2),t2后执行park()阻塞自己,t2不就一直阻塞了吗??
这是park和unpark方法中一个好玩的地方:park和unpark方法是有计数的,哪个方法在前面无所谓。如果先调用了unpark方法,会给这个线程一个标记"有人唤醒过你",再调用park方法,该线程是不会阻塞的。
2.3 代码示例
// 两个线程交替输出
public class demo01 {
static Thread t1,t2;
public static void main(String[] args) {
char []a=new char[]{'1','2','3'};
char []b=new char[]{'a','b','c'};
t1=new Thread(new Runnable() {
@Override
public void run() {
for (char c : a) {
System.out.print(c);
LockSupport.unpark(t2); //唤醒T2
LockSupport.park(); //当前线程T1阻塞
}
}
});
t2=new Thread(new Runnable() {
@Override
public void run() {
for (char c : b) {
LockSupport.park(); //当前线程T2阻塞
System.out.print(c); //唤醒T1
LockSupport.unpark(t1);
}
}
});
t1.start();
t2.start();
}
}
3、使用ReentrantLock和Condition
3.1 整体流程解读:
先试用lock()方法尝试获取锁,获取到锁的线程输出对应数字or字母,然后唤醒另一个队列中的线程,阻塞当前队列线程。【如果要确定先输出数字,还是用CountDownLatch门栓,如果t2线程先运行,就会卡在门栓这一步,等到t1正常输出,打开门栓,t2才能正常尝试获取lock锁输出。】
3.2 疑问解答:
1、为什么要使用两个队列conditionT1和conditionT2?
以生产者-消费者模型为例,我想要明确唤醒生产者或明确唤醒消费者。使用一般的wait()、notify()就是随机唤醒,没有办法指定。因此使用两个队列,想唤醒哪种线程,就调用哪个队列的signal()方法。
3.3 代码示例
// 两个线程交替输出
public class demo03 {
private static CountDownLatch latch=new CountDownLatch(1);
public static void main(String[] args) {
char []a=new char[]{'1','2','3'};
char []b=new char[]{'a','b','c'};
Lock lock=new ReentrantLock();
Condition conditionT1=lock.newCondition();
Condition conditionT2=lock.newCondition();
new Thread(()->{
lock.lock();
try {
for (char c : a) {
System.out.println(c);
latch.countDown();
conditionT2.signal();
conditionT1.await();
}
conditionT2.signal();
}catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t1").start();
new Thread(()->{
latch.await();
lock.lock();
try {
for (char c : b) {
System.out.println(c);
conditionT1.signal();
conditionT2.await();
}
conditionT1.signal();
}catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t2").start();
}
}
4、使用阻塞队列TransferQueue
4.1 整体流程解读:
TransferQueue是一个同步容器,它的容量为0,一旦有线程调用transfer(xxx)方法放进去东西之后,它就会阻塞。直到另一个线程把数据拿出后,才会继续。同样的,线程调用take()方法从队列中拿东西,也是阻塞的,必须有人把东西放在队列里,才会继续。【保证有人消费了我的数据,我才继续;保证有人给我数据,我才继续。】
**好玩的点:**本来是第一个线程输出数字,第二个线程输出字母。现在是第二个线程把字母交给队列,第一个线程去队列中取,输出。
4.2 代码示例
// 两个线程交替输出
public class demo04 {
public static void main(String[] args) {
char []a=new char[]{'1','2','3'};
char []b=new char[]{'a','b','c'};
TransferQueue<Character> transferQueue=new LinkedTransferQueue<>();
new Thread(()->{
try {
for (char c : b) {
System.out.println(transferQueue.take());
transferQueue.transfer(c);
}
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t1").start();
new Thread(()->{
try {
for (char c : a) {
transferQueue.transfer(c);
System.out.println(transferQueue.take());
}
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t2").start();
}
}