如何实现2个线程交替循环打印,如2个线程交替打印1A2B3C4D5E6F7G,其中线程1输出数字,线程2输出字母。再加大点难度,如何实现多个线程交替循环打印?如线程1打印A,线程2打印B,线程3打印C,要求交替打印,并且可以循环打印,输出结果类似:ABCABCABC。如果是第一次见到这道题,很难在短时间内想出合理的解决方案。 如果只要求交替打印一次的话,实现比较简单,可以用Thread.join()方法,一个线程等待另一个线程执行完成。 现在要求循环打印,就涉及线程间通信,必须要用到锁。
1、两个线程交替执行
两个线程如何交替打印输出?这个问题可以有助于快速理解并发相关的API的使用,以及相关的原理。下面我们用多种方式实现这个问题。
具体问题:两个线程交替输出,第一个线程:1 2 3 4 5 6 7;第二个线程: A B C D E F G;输出结果:A2B3C4D5E6F7G。
1.1 通过加锁和notify()、wait()机制
1.1.1 方式一
public class TwoThreadPrint {
public static void main(String[] args) {
MutilThreadPrint m1 = new MutilThreadPrint();
Thread thread1 = new Thread(m1);
Thread thread2 = new Thread(m1);
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
class MutilThreadPrint implements Runnable {
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
int num = 0;
@Override
public void run() {
synchronized (this) {
if(num++ % 2 == 0) {
for(char num: nums) {
print(num);
}
} else {
for(char char1: chars) {
print(char1);
}
}
}
}
public void print(char num) {
// 唤醒wait()的一个或所有线程
this.notify();
System.out.println(Thread.currentThread().getName() + ":" + num);
try {
// wait会释放当前的锁,让另一个线程可以进入
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过加锁和notify()、wait()机制可以有效的实现两个线程交替打印不同的数组,但互斥锁始终会影响性能,效率不高。
1.1.2 方式二
synchronized的原理,多个线程同时抢一把锁,该锁维护一个队列(同步队列),没有争抢到锁的所有线程在同一个同步队列里。
- wait:该线程释放锁后阻塞,该线程进入另一个队列(等待队列)。
- notify:随机叫醒等待队列中的任何一个线程。
- notifyAll: 唤醒等待队列中的所有线程,让他们去争夺同一把锁,谁争抢到谁执行。
public class TwoThreadPrint {
// 使用Object的notify和wait
public static void main(String[] args) {
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
Object lock = new Object();
/**
* 线程t1和t2同时争抢同一把锁
*/
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();
}
}
该例中,线程t1和t2同时争抢同一把锁:
- 假如t1争抢到锁,则t1执行而t2进入到该锁的同步队列中,
- t1执行时,执行到notify,这时等待队列中没有线程,继续执行到wait时,释放锁并让t1进入到等待队列,等待notify唤醒
- 由于t1释放锁了,同步队列中的t2就可以获得锁执行,执行到notify时,会随机唤醒等待队列中的一个线程,因为队列中只有一个t1,所以t1去争抢锁;t2执行到wait,t2释放锁并让t2进入到等待队列,等待notify唤醒,t1会获得锁执行。
- 依次循环
这种方式有两个问题:
- 问题1:启动时不一定是t1先获得锁,可能会先打印字母。如何保证t1先获得锁呢?
- 问题2:这个线程会停止吗? 不会,最终等待队列中一定有一个线程存在。
1.1.3 使用CountDownLatch改进方式二
import java.util.concurrent.CountDownLatch;
public class TwoThreadPrint {
public static CountDownLatch countDownLatch = new CountDownLatch(1);
// 使用Object的notify和wait, 改进方法二的实现
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() 计算调用次数
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() 能够阻塞线程 直到调用N次countDownLatch.countDown() 方法才释放线程
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();
}
}
1.1.4 使用valatile标志位改进方式二
public class TwoThreadPrint {
public static volatile boolean flag = true;
// 使用Object的notify和wait, 改进方法二的实现
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) {
if (flag) {
for (char temp : nums) {
System.out.println(temp);
flag = false;
lock.notify();
lock.wait();
}
lock.notify(); //清空等待队列
}
}
} catch (Exception e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
synchronized (lock) {
if (!flag) {
for (char temp : chars) {
System.out.println(temp);
flag = true;
lock.notify();
lock.wait();
}
lock.notify(); //清空等待队列
}
}
} catch (Exception e) {
e.printStackTrace();
}
}, "t2");
t1.start();
t2.start();
}
}
1.2 使用LockSupport的unpark和park
LockSupport是concurrent包中一个工具类,是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。本例中我们主要使用 park 和 unpark 方法。
LockSupport常用方法
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
这儿park和unpark其实实现了wait和notify的功能,不过还是有一些差别的。
- park不需要获取某个对象的锁。
- 因为中断的时候park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理。
- park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
- park和unpark的先后顺序并不是那么严格,park和unpark的使用不会出现死锁的情况。unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
LockSupport常用方法
import java.util.concurrent.locks.LockSupport;
/**
* 两个线程交替打印
*/
public class TwoThreadPrint {
// 定义为static
static Thread t1 = null;
static Thread t2 = null;
// 使用LockSupport的unpark和park
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();
}
}
1.3 使用TransferQueue
这是一种取巧的方式,利用阻塞队列TransferQueue,队列里最多只能放一个元素,而且一个线程调用transfer放元素的时候,如果没有另一个线程调用take去取走元素,则放元素的线程必须阻塞在这个位置。
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;
/**
* 两个线程交替打印
*/
public class TwoThreadPrint4 {
// 使用TransferQueue
public static void main(String[] args) {
// 阻塞队列,队列里最多只能放一个元素,
// 而且一个线程调用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();
}
}
1.4 使用ReentrantLock和CountDownLatch
本例中试用了公平锁,避免同一个线程多次获取锁的情况来实现顺序打印,同时使用countDownLatch让两个线程同时开始。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TwoThreadPrint {
CountDownLatch countDownLatch = new CountDownLatch(1);
Lock lock = new ReentrantLock(true);
volatile int num = 0;
public void printA() {
lock.lock();
try {
num++;
System.out.println("current Thread A:" + Thread.currentThread()+num);
}catch (Exception e){
}finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
num++;
System.out.println("current Thread B:" + Thread.currentThread()+num);
}catch (Exception e){
}finally {
lock.unlock();
}
}
// 使用Object的notify和wait, 改进方法二的实现
public static void main(String[] args) {
TwoThreadPrint print = new TwoThreadPrint();
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
new Thread(new Runnable() {
@Override
public void run() {
try{
print.countDownLatch.await();
}catch (Exception e){
e.printStackTrace();
}
for (int i = 0; i < 50; i++) {
print.printA();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
print.countDownLatch.countDown();;
for (int i = 0; i < 50; i++) {
print.printB();
}
}
}).start();
}
}
1.5 使用ReentrantLock和Condition
使用ReentrantLock和Condition, 是一种比较好的实现方式。可以很方便的使用多个线程交替打印。也需要使用CountDownLantch精确的让哪一个线程先执行。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TwoThreadPrint9 {
// 使用ReentrantLock和Condition, 是一种比较好的实现方式。可以很方便的使用多个线程交替打印。
public static void main(String[] args) {
char[] nums = "1234567".toCharArray();
char[] chars = "ABCDEFG".toCharArray();
/**
* 每一个Condition都有一个队列,cT1的条件队列里有线程t1,cT2的条件队列里有线程t2。每个队列里的线程都是通过await释放锁,通过signal唤醒。
*
* 可以使用多个Condition精确的控制程序的打印.
*
* 也需要使用CountDownLantch精确的让哪一个线程先执行
*
*/
ReentrantLock lock = new ReentrantLock();
Condition cT1 = lock.newCondition();
Condition cT2 = 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);
// 通知第一个线程执行,让第二个线程等待
cT1.signal();
cT2.await();
}
cT1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
}
}
2、多个线程交替执行
本例中主要介绍3个线程交替执行的例子,再多线程的原理是一样的。
2.1 使用ReentrantLock和Condition
同样是使用ReentrantLock和Condition, 也需要使用CountDownLantch精确的让哪一个线程先执行。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ThreeThreadPrint {
// 使用ReentrantLock和Condition, 是一种比较好的实现方式。可以很方便的使用多个线程交替打印。
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();
}
}
2.2 使用ReentrantLock和Condition
线程1打印A,线程2打印B,线程3打印C,要求交替打印,并且可以循环打印。输出结果类似:ABCABCABC
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreeThreadPrint1 {
private static Lock lock = new ReentrantLock();
private static Condition A = lock.newCondition();
private static Condition B = lock.newCondition();
private static Condition C = lock.newCondition();
private volatile static int count = 0;
private static int sum=1;
static class ThreadA extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
if (count % 3 != 0){//注意这里是不等于0,也就是说没轮到该线程执行,之前一直等待状态
A.await(); //该线程A将会释放lock锁,构造成节点加入等待队列并进入等待状态
}
System.out.println("-------第"+sum+"次--------");
System.out.println("A");
count++;
B.signal(); // A执行完唤醒B线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class ThreadB extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
if (count % 3 != 1)
B.await();// B释放lock锁,当前面A线程执行后会通过B.signal()唤醒该线程
System.out.println("B");
count++;
C.signal();// B执行完唤醒C线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class ThreadC extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
if (count % 3 != 2)
C.await();// C释放lock锁
System.out.println("C");
count++;
sum++;
A.signal();// C执行完唤醒A线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
new ThreadA().start();
new ThreadB().start();
new ThreadC().start();
}
}
2.3 使用Semaphore信号量
前2个多线程循环打印使用Synchronized或者ReentrantLock实现,它们不能控制线程启动后的执行顺序。因为三个线程启动后,都等待CPU调度执行,而CPU调度的顺序又是随机的,所以不能保证线程1先执行。如果想要控制打印顺序,需要使用CountDownLantch。
有个可以控制线程启动后执行顺序,又简单的实现方式,就是用Semaphore(信号量),它可以控制共享资源的访问个数。本例使用Semaphore信号量来精确控制线程的打印顺序。
使用方式:
初始化的时候,指定共享资源的个数
// 初始化一个资源
Semaphore semaphore = new Semaphore(1);
获取资源,获取资源后,semaphore资源个数减1,变成0,其他线程再获取资源的时候就会阻塞等待
semaphore.acquire();
释放资源,semaphore资源个数加1,其他阻塞的线程就可以获取到资源了
semaphore.release();
实现代码
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreeThreadPrint2 {
static class ThreadDemo extends Thread {
private Semaphore current;
private Semaphore next;
private String name;
/**
* 构造方法
*
* @param current 要获取的当前锁
* @param next 要释放的下一把锁
* @param name 打印内容
*/
public ThreadDemo(Semaphore current, Semaphore next, String name) {
this.current = current;
this.next = next;
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
// 获取当前锁,然后打印
current.acquire();
System.out.print(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 释放下一把锁
next.release();
}
}
}
public static void main(String[] args) {
// 初始化三把锁,只有A锁是可用的
Semaphore A = new Semaphore(1);
Semaphore B = new Semaphore(0);
Semaphore C = new Semaphore(0);
// 创建并启动三个线程,线程1获取A锁,释放B锁
new ThreadDemo(A, B, "A").start();
// 线程2获取B锁,释放C锁
new ThreadDemo(B, C, "B").start();
// 线程3获取C锁,释放A锁
new ThreadDemo(C, A, "C").start();
}
}
评论区