假设我现在有三个字符串,abcdefg,1234567,!@#$%^&,我期望使用多线程来进行打印,打印的结果为a1!b2@c3#d4$e5%f6^g7&。这也是道面试题。
有三种方案,synchronized,ReentrantLock结合Condition,LockSupport的park,unpark方法三种。
这个题的思路是要做什么,使用多线程,并且控制执行线程的执行顺序,来完成对结果的打印。
控制线程执行顺序,肯定要用到线程的通信机制。
Synchronized方式
synhronized中配合锁对象中的wait()以及notify,notifyAll方法,可以完成这种机制。但是notify只可能唤醒一个随机的线程,这个肯定不能用。而notifyAll会唤醒所有当前锁的wait线程,那么为了让唤醒的线程是我想执行的,那么就需要一个条件状态来进行控制。举个例子。
比如现在定义了个线程共享的成员变量,该变量有三个状态,0,1,2,有三个线程A,B,C来分别对应这三个状态,执行这三种状态的数据。我现在的想法是A执行完成后,执行B,接着执行C,再执行A的这种闭环操作,直至最终执行完成。那么这个变量,初始值为0,让它进入了我A线程,在执行完A线程的操作后,将该变量置为1,调用notifyAll方法,唤醒B,C线程,此时B,C线程判断该变量是否满足自身的执行条件,B发现满足,接着走下面的方法。C发现不满足,继续调用wait方法阻塞。
Lock+Condition方式
与Condition不同,ReentrantLock中可以包含多个Condition,类似Synchronized中的waitSet。那么就有这么一个思路了。现在有A,B,C三个线程,分别轮询执行三个字符串。那么我也可以定义三个Condition,默认情况下都是await阻塞状态。在主线程中,我先把A线程对应的confition使用signal方法来进行唤醒,执行A的业务逻辑,在A的业务逻辑执行完成,在A线程中唤醒B的condition,此时B线程开始执行。然后B的业务执行完成后,唤醒C的condition中signal方法。依次执行,直至程序执行完成。
LockSupport中的park,unpark方式
这种方式和Lock+Condition方式类似。只不过Lock+Condition唤醒的是一个Condition中的唯一一个线程,而park,unpark直接操作于线程。但在执行过程中存在个问题,使用unpark(线程名)唤醒线程时,会出现线程为空的现象,这个得后续去好好的了解下。
代码
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class ThreadPrintDataTest {
//多线程打印交替数据问题
private static String a = "abcdefg";
private static String b = "1234567";
private static String c = "!@#$%^&";
/**
* 使用Synchronized来解决打印数据交错问题
*/
@Test
public void synchronizedTest() throws InterruptedException {
Object lock = new Object();
char[] aArr = a.toCharArray();
char[] bArr = b.toCharArray();
char[] cArr = c.toCharArray();
//这种方案该怎么处理,设置一个状态,在a打印完成后,状态设置为b,b打印完成之后,状态设置为c,c打印完成后,状态设置为a
//状态设置为0,1,2
new Thread(() -> {
for (int i = 0; i < aArr.length; i++) {
printSynchronized(aArr, 0, i, lock);
}
}, "t1").start();
new Thread(() -> {
for (int i = 0; i < aArr.length; i++) {
printSynchronized(bArr, 1, i, lock);
}
}, "t2").start();
new Thread(() -> {
for (int i = 0; i < aArr.length; i++) {
printSynchronized(cArr, 2, i, lock);
}
}, "t3").start();
while (true) {
Thread.sleep(1000);
}
}
private volatile Integer status = 0;
/**
* synchronized轮询打印的状态
*
* @param arr 数组
* @param curStatus 当前状态
* @param index index下标
* @param lock 使用的锁
*/
private void printSynchronized(char[] arr, int curStatus, int index, Object lock) {
//状态应该是公用的
synchronized (lock) {
//状态不相等,先行阻塞
while (curStatus != status) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//状态相等的情况
System.out.print(arr[index]);
if (status == 2) {
status = 0;
} else {
status++;
}
lock.notifyAll();
}
}
/**
* 使用Lock配合Condition来进行使用
*/
@Test
public void lockConditionTest() throws InterruptedException {
//这个思路可以采用,一个Lock名下有多个Condition,不同的元素放到不同的condition中,完成操作
ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
char[] aArr = a.toCharArray();
char[] bArr = b.toCharArray();
char[] cArr = c.toCharArray();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < aArr.length; i++) {
printCondition(condition1, condition2, aArr, i, lock);
}
}
}, "t1");
t1.start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < aArr.length; i++) {
//waitSet存放的是线程ID,这个里面存放的应该也是线程ID,它肯定只存放一个线程ID的
printCondition(condition2, condition3, bArr, i, lock);
}
}
}, "t2").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < aArr.length; i++) {
printCondition(condition3, condition1, cArr, i, lock);
}
}
}, "t3").start();
//condition和lock看来与synchronized调用类似,必须放到lock方法中访问,明白了
lock.lock();
try {
condition1.signal();
} finally {
lock.unlock();
}
Thread.sleep(100000);
}
//老师写的面向对象,比我这个写的要好看,我没有面向对象的思想,不过思路对的
private void printCondition(Condition curCondition, Condition nextCondition, char[] arr, int index, ReentrantLock lock) {
lock.lock();
try {
//当前Condition正在等待
curCondition.await();
System.out.print(arr[index]);
nextCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 基于LockSupport的park,unpark机制来完成
*/
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
char[] aArr = a.toCharArray();
char[] bArr = b.toCharArray();
char[] cArr = c.toCharArray();
//初始状况下都是暂停的,在外部控制来给他放行,老师封装的目的主要是为了线程可以初始化完成吗?
//这么写会存在一个问题,来线程对象未定义好的时候,就唤醒,这中方式是不对的
t1 = new Thread(new Runnable() {
@Override
public void run() {
for (char c1 : aArr) {
lockSupportPrint(c1,lock, Thread.currentThread(), t2);
}
}
});
t2 = new Thread(new Runnable() {
@Override
public void run() {
for (char c1 : bArr) {
lockSupportPrint(c1,lock, Thread.currentThread(), t3);
}
}
});
t3 = new Thread(new Runnable() {
@Override
public void run() {
for (char c1 : cArr) {
lockSupportPrint(c1,lock,Thread.currentThread(), t1);
}
}
});
//线程启动顺序的问题
//TODO 这里存在个问题,线程变量必须定义成静态变量,start()启动必须得放到后面,要不传入的线程对像可能为空,
//后期去看看什么问题吧
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
//不需要锁,写懵了
private static void lockSupportPrint(char value, ReentrantLock lock, Thread curThread, Thread nextThread){
//暂停当前线程
LockSupport.park();
System.out.print(value);
//唤醒下一个线程
LockSupport.unpark(nextThread);
}
}
第四天的课程终于完了,下来准备第五天的课程,除了第八天,剩下的都不多了,加油,尽快。