两个线程如何交替打印输出?这个问题可以有助于快速理解并发相关的API的使用,以及相关的原理。
具体题目如下:
两个线程交替输出
第一个线程:1 2 3 4 5 6 7
第二个线程: A B C D E F G
输出结果:1A2B3C4D5E6F7G
方法一:使用LockSupport的unpark和park
package com.wuxiaolong.concurrent;
import java.util.concurrent.locks.LockSupport;
/**
* Description:
* 两个线程交替输出
* 第一个线程:1 2 3 4 5 6 7
* 第二个线程: A B C D E F G
* 输出结果:1A2B3C4D5E6F7G
*
* @author 诸葛小猿
* @date 2021-03-01
*/
public class TestSample1 {
// 定义为static
static Thread t1 = null;
static Thread t2 = null;
public static void main(String[] args) {
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
t1 = new Thread(()->{
for(char temp: nums){
System.out.println(temp);
LockSupport.unpark(t2); // 叫醒t2
LockSupport.park(); // 阻塞当前线程t1
}
},"t1");
t2 = new Thread(()->{
for(char temp: chars){
LockSupport.park(); // 阻塞当前线程t2
System.out.println(temp);
LockSupport.unpark(t1); // 叫醒t1
}
},"t2");
t1.start();
t2.start();
}
}
方法二:使用Object的notify和wait
package com.wuxiaolong.concurrent;
import java.util.concurrent.locks.LockSupport;
/**
* Description:
* 两个线程交替输出
* 第一个线程:1 2 3 4 5 6 7
* 第二个线程: A B C D E F G
* 输出结果:1A2B3C4D5E6F7G
*
* @author 诸葛小猿
* @date 2021-03-01
*/
public class TestSample2 {
public static void main(String[] args){
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
Object lock = new Object();
/**
* synchronized的原理,多个线程同时抢一把锁,该锁维护一个队列(同步队列),没有争抢到锁的所有线程在同一个同步队列里。
* wait:该线程释放锁后阻塞,该线程进入另一个队列(等待队列)
* notify:随机叫醒等待队列中的任何一个线程
* notifyAll: 唤醒等待队列中的所有线程,让他们去争夺同一把锁,谁争抢到谁执行。
*
* 下面t1和t2同时争抢同一把锁:
* 1.假如t1争抢到锁,则t1执行而t2进入到该锁的同步队列中,
* 2.t1执行时,执行到notify,这时等待队列中没有线程,继续执行到wait时,释放锁并让t1进入到等待队列,等待notify唤醒
* 3.由于t1释放锁了,同步队列中的t2就可以获得锁执行,执行到notify时,会随机唤醒等待队列中的一个线程,因为队列中只有一个t1,所以t1去争抢锁;t2执行到wait,t2释放锁并让t2进入到等待队列,等待notify唤醒,t1会获得锁执行。
* 4.依次循环
*/
Thread t1 = new Thread(()->{
try{
synchronized (lock){
for(char temp: nums){
System.out.println(temp);
lock.notify();
lock.wait();
}
}
}catch (Exception e){
}
},"t1");
Thread t2 = new Thread(()->{
try{
synchronized (lock){
for(char temp: chars){
System.out.println(temp);
lock.notify();
lock.wait();
}
}
}catch (Exception e){
}
},"t2");
t1.start();
t2.start();
}
}
这种方式有两个问题:
- 问题1:启动时不一定是t1先获得锁,可能会先打印字母。如何保证t1先获得锁呢? 使用CountDownLatch。
- 问题2:这个线程会停止吗? 不会,最终等待队列中一定有一个线程存在。
方法三:改进方法二的实现
package com.wuxiaolong.concurrent;
import java.util.concurrent.CountDownLatch;
/**
* Description:
* 两个线程交替输出
* 第一个线程:1 2 3 4 5 6 7
* 第二个线程: A B C D E F G
* 输出结果:1A2B3C4D5E6F7G
*
* @author 诸葛小猿
* @date 2021-03-01
*/
public class TestSample2_1 {
public static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args){
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
Object lock = new Object();
Thread t1 = new Thread(()->{
try{
synchronized (lock){
countDownLatch.countDown(); //减去1
for(char temp: nums){
System.out.println(temp);
lock.notify();
lock.wait();
}
lock.notify(); //清空等待队列
}
}catch (Exception e){
e.printStackTrace();
}
},"t1");
Thread t2 = new Thread(()->{
try{
countDownLatch.await(); //等待,下面的代码一定不会先运行。这里使用的是await,不是wait
synchronized (lock){
for(char temp: chars){
System.out.println(temp);
lock.notify();
lock.wait();
}
lock.notify(); //清空等待队列
}
}catch (Exception e){
e.printStackTrace();
}
},"t2");
t1.start();
t2.start();
}
}
方法四:使用ReentrantLock和Condition
这是一种比较好的实现方式。可以很方便的使用多个线程交替打印。
package com.wuxiaolong.concurrent;
import java.sql.SQLOutput;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Description:
* 两个线程交替输出
* 第一个线程:1 2 3 4 5 6 7
* 第二个线程: A B C D E F G
* 第三个线程: ~!@#$%^
* 输出结果:1A!2B@3C#4D$5E%6F^7G&
*
* @author 诸葛小猿
* @date 2021-03-01
*/
public class TestSample3 {
public static void main(String[] args){
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
char[] signs = "~!@#$%^".toCharArray();
/**
* 每一个Condition都有一个队列,cT1的条件队列里有线程t1,cT2的条件队列里有线程t2。每个队列里的线程都是通过await释放锁,通过signal唤醒。
*
* 可以使用多个Condition精确的控制程序的打印.
*
* 也需要使用CountDownLantch精确的让哪一个线程先执行
*
*/
ReentrantLock lock = new ReentrantLock();
Condition cT1 = lock.newCondition();
Condition cT2 = lock.newCondition();
Condition cT3 = lock.newCondition();
Thread t1 = new Thread(()->{
lock.lock();
try{
for (char temp : nums){
System.out.println(temp);
// 通知第二个线程执行,让第一个线程等待
cT2.signal();
cT1.await();
}
cT2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
});
Thread t2 = new Thread(()->{
lock.lock();
try{
for (char temp : chars){
System.out.println(temp);
// 通知第三个线程执行,让第二个线程等待
cT3.signal();
cT2.await();
}
cT3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
});
Thread t3 = new Thread(()->{
lock.lock();
try{
for (char temp : signs){
System.out.println(temp);
// 通知第一个线程执行,让第三个线程等待
cT1.signal();
cT3.await();
}
cT1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
});
t1.start();
t2.start();
t3.start();
}
}
方法五:使用TransferQueue
这是一种很取巧的做法。
package com.wuxiaolong.concurrent;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;
/**
* Description:
* 两个线程交替输出
* 第一个线程:1 2 3 4 5 6 7
* 第二个线程: A B C D E F G
* 输出结果:1A2B3C4D5E6F7G
*
* @author 诸葛小猿
* @date 2021-03-01
*/
public class TestSample4 {
public static void main(String[] args) throws Exception{
// 阻塞队列,队列里最多只能放一个元素,
// 而且一个线程调用transfer放元素的时候,如果没有另一个线程调用take去取走元素,则放元素的线下必须阻塞在这个位置
TransferQueue<Character> tq = new LinkedTransferQueue<>();
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
Thread t1 = new Thread(()->{
try{
for(char temp: nums){
tq.transfer(temp); // 当前线程放一个元素进队列,并阻塞在这里,等待另一个线程拿走这个元素,才能往下执行
System.out.println(tq.take());// 从队列中拿走另一个线程放的元素,如果没有元素也会阻塞
}
}catch (Exception e){
e.printStackTrace();
}
},"t1");
Thread t2 = new Thread(()->{
try{
for(char temp: chars){
System.out.println(tq.take());
tq.transfer(temp);
}
}catch (Exception e){
e.printStackTrace();
}
},"t2");
t2.start();
t1.start();
}
}