使用两个线程,一个线程打印1-26,另一个打印A-Z,交替打印。
看到题目首先想到的就是wait notify,线程1每打印一次就等待,并唤醒线程2,线程2打印完,也等待并唤醒线程1
代码实现:
首先准备数据
public class PrintDemo {
private static final int LENGTH = 26;
private static int[] arrI = new int[LENGTH];
private static char[] arrC = new char[LENGTH];
//init data
static{
for (int i = 0; i < LENGTH; i++) {
arrI[i] = i+1;
arrC[i] = (char) ('A' + i);
}
}
//...
wait notify方式:启动两个线程轮流打印。代码注意的点就是,wait 和 notify方法必须在同步块内使用,for循环结束要notify阻塞线程,否则程序不会退出。
public void printWait(){
new Thread(() -> {
synchronized (this){
for (int i : arrI) {
System.out.print(i);
try {
this.notify();
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//notify blocked Thread after print otherwise the program won't exit
this.notify();
}
},"t1").start();
new Thread(() -> {
synchronized (this){
for (char i : arrC) {
try {
System.out.print(i);
this.notify();
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//notify blocked Thread after print otherwise the program won't exit
this.notify();
}
},"t2").start();
}
最终程序输出
1A2B3C4D5E6F7G8H9I10J11K12L13M14N15O16P17Q18R19S20T21U22V23W24X25Y26Z
如果要控制字母先打印,可以用CountDownLatch控制,当然也可以在t1线程把wait提到print前面,下面贴出用CountDownLatch的代码:
public void printWaitCDL(){
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){
for (int i : arrI) {
System.out.print(i);
try {
this.notify();
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//notify blocked Thread after print otherwise the program won't exit
this.notify();
}
},"t1").start();
new Thread(() -> {
synchronized (this){
for (char i : arrC) {
try {
System.out.print(i);
latch.countDown();
this.notify();
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//notify blocked Thread after print otherwise the program won't exit
this.notify();
}
},"t2").start();
}
注意不能把latch.await()放到synchronized里面,否则会死锁。
Thread t1 = null , t2 = null;
public void printLockSupport(){
t1 = new Thread(()->{
for (int i : arrI) {
System.out.print(i);
LockSupport.unpark(t2);
LockSupport.park();
}
},"t1");
t2 = new Thread(()->{
for (char i : arrC) {
LockSupport.park();
System.out.print(i);
LockSupport.unpark(t1);
}
},"t1");
t1.start();
t2.start();
}
原理是一样的,只不过LockSupport不用必须在synchronized里面使用,LockSupport允许精准唤醒某个线程。
利用Atomic类的可见性,也可以完成功能,思路是设置一个共享变量控制当前轮到哪个线程执行。
static volatile boolean t1turn = true;
static AtomicInteger count = new AtomicInteger(0);
void printAtomic(){
new Thread(()->{
while (count.get()<LENGTH){
while (t1turn){
System.out.print(arrI[count.get()]);
t1turn = false;
}
}
},"t1").start();
new Thread(()->{
while (count.get()<LENGTH){
while (!t1turn){
System.out.print(arrC[count.getAndIncrement()]);
t1turn = true;
}
}
},"t2").start();
}
这里设置了两个共享变量,一个用于控制线程执行权,一个用作循环的计数。这里volatile和Atomic可以起到同样的效果,关键就是保证变量的可见性。
上面的实现方式可用一个忙循环改造一下。
enum Turn {T1,T2}
volatile Turn turn = Turn.T1;
void printVolatile(){
new Thread(()->{
for (int i : arrI) {
while (turn != Turn.T1){}
System.out.print(i);
turn = Turn.T2;
}
},"t1").start();
new Thread(()->{
for (char i : arrC) {
while (turn != Turn.T2){}
System.out.print(i);
turn = Turn.T1;
}
},"t2").start();
}
以上两种方式区别就是不进行线程挂起,通过循环判断volatile共享变量的状态来实现交替打印,实际上并没有线程通信。
wait notify + synchronized 这一套也可以用 ReentrantLock的Condition来代替:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
void printLockCondition(){
new Thread(()->{
try {
lock.lock();
for (int i : arrI) {
System.out.print(i);
condition.signal();
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//let other Thread run, and unlock
condition.signal();
lock.unlock();
}
},"t1").start();
new Thread(()->{
try {
lock.lock();
for (char i : arrC) {
System.out.print(i);
condition.signal();
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
condition.signal();
lock.unlock();
}
},"t2").start();
}
可以用两个Condition,优化一下Condition的实现方式,实现精准唤醒某个线程。
ReentrantLock lock2 = new ReentrantLock();
Condition conditionT1 = lock2.newCondition();
Condition conditionT2 = lock2.newCondition();
void printLock2Condition(){
new Thread(()->{
try {
lock.lock();
for (int i : arrI) {
System.out.print(i);
conditionT2.signal();
conditionT1.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//let other Thread run, and unlock
conditionT2.signal();
lock.unlock();
}
},"t1").start();
new Thread(()->{
try {
lock.lock();
for (char i : arrC) {
System.out.print(i);
conditionT1.signal();
conditionT2.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
conditionT1.signal();
lock.unlock();
}
},"t2").start();
}
要实现线程阻塞自然可以想到阻塞队列,利用BlockingQueue的take阻塞方法,也可以实现两个线程交替输出。
static BlockingQueue<Integer> q1 = new ArrayBlockingQueue<>(1);
static BlockingQueue<Character> q2 = new ArrayBlockingQueue<>(1);
void printBlockingQueue(){
new Thread(()->{
for (int i : arrI) {
try {
q1.put(i);
System.out.print(q2.take());//blocked
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
new Thread(()->{
for (char i : arrC) {
try {
System.out.print(q1.take());
q2.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();
}
BlockingQueue的take方法是获取的时候阻塞,还可以用TransferQueue的transfer方法,放入就阻塞,直到元素被取走。
static TransferQueue<String> tq = new LinkedTransferQueue<>();
void printTransferQueue(){
new Thread(()->{
for (int i : arrI) {
try {
tq.transfer(String.valueOf(i));
System.out.print(tq.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1").start();
new Thread(()->{
for (char i : arrC) {
try {
System.out.print(tq.take());
tq.transfer(String.valueOf(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();
}
两个线程交替执行,可以利用wait notify方法,volatile变量线程可见性,Lock API,LockSupport的park unpark方法,阻塞队列API以及其他方式来实现。