引言
不知道最近有没有小伙伴去面试,今天了不起回想到了早期去面试遇到的一个多线程面试问题。
面试问题是一个笔试题:
两个线程依次交替输出A~Z,1到26,形如A1B2C3D4...
当时的我还很菜,用了原生的线程,借助wait和notify方法实现。
伙伴们你们也可以先暂停,自己思考下用什么方式来实现。
今天了不起和伙伴们一起来基于JDK1.8进行实现方式的探索,请看下文。
1. 使用线程方法
wait()方法会使当前线程释放锁,并进入等待状态,直到以下情况之一发生:
- 被其他线程调用notify()方法唤醒;
- 被其他线程调用notifyAll()方法唤醒;
- 被其他线程中断。
notify()方法用于唤醒一个正在等待的线程,使其从wait()方法中返回。
结合一个出让等待的机制,就这样交替实现。
public class T06_00_sync_wait_notify {
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
new Thread(()->{
synchronized (o) {
for(char c : aI) {
System.out.print(c);
try {
o.notify();
o.wait(); //让出锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify(); //必须,否则无法停止程序
}
}, "t1").start();
new Thread(()->{
synchronized (o) {
for(char c : aC) {
System.out.print(c);
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}
运行结果:
图片
思考:伙伴们,如果我想保证t2在t1之前打印,也就是说保证首先输出的是A而不是1,这个时候该如何做?
2. 使用CountDownLatch铁门闩
CountDownLatch是Java多线程中的一个同步工具类,它可以让一个或多个线程等待其他线程完成操作后再继续执行。
具体来说,CountDownLatch有两个主要方法:
- await()方法:调用该方法的线程会进入等待状态,直到计数器的值为0或者被中断;
- countDown()方法:调用该方法会将计数器减1,当计数器的值为0时,会唤醒所有等待的线程。
public class T07_00_sync_wait_notify {
private static CountDownLatch latch = new CountDownLatch(1);
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
new Thread(()->{
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o) {
for(char c : aI) {
System.out.print(c);
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t1").start();
new Thread(()->{
synchronized (o) {
for(char c : aC) {
System.out.print(c);
latch.countDown();
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}
运行结果:
图片
3. 使用ReentrantLock
我们可以通过ReentrantLock获取条件锁,通过它提供的方法来实现。
具体来说,ReentrantLock的Condition接口提供了以下三个方法:
- await()方法:当前线程进入等待状态,并释放锁,直到其他线程使用signal()或signalAll()方法唤醒它;
- signal()方法:唤醒一个等待在该条件上的线程;
- signalAll()方法:唤醒所有等待在该条件上的线程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T08_00_lock_condition {
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(()->{
try {
lock.lock();
for(char c : aI) {
System.out.print(c);
condition.signal();
condition.await();
}
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(()->{
try {
lock.lock();
for(char c : aC) {
System.out.print(c);
condition.signal();
condition.await();
}
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
运行结果:
图片
Condition本质是锁资源上不同的等待队列,我们也可以获取不同的等待队列来实现。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T09_00_lock_condition {
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
Lock lock = new ReentrantLock();
Condition conditionT1 = lock.newCondition();
Condition conditionT2 = lock.newCondition();
new Thread(()->{
try {
lock.lock();
for(char c : aI) {
System.out.print(c);
conditionT2.signal();
conditionT1.await();
}
conditionT2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(()->{
try {
lock.lock();
for(char c : aC) {
System.out.print(c);
conditionT1.signal();
conditionT2.await();
}
conditionT1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
4. 使用TransferQueue阻塞队列
TransferQueue是Java并发包中的一个阻塞队列,它可以用于多线程之间的数据交换和同步。
LinkedTransferQueue继承自TransferQueue,并且还可以支持异步操作。
图片
LinkedTransferQueue的take()方法和transfer()方法都是用于从队列中取出元素的方法,但它们的使用场景和行为有所不同。
take()方法是一个阻塞方法,它会一直阻塞直到队列中有可用元素,才将队列中的元素取出并返回。
transfer()方法也是一个阻塞方法,它会将指定的元素插入到队列中,并等待另一个线程从队列中取出该元素。如果队列中没有等待的线程,则当前线程会一直阻塞,直到有其他线程从队列中取走该元素为止。
那么我们就利用这一点它必须要另外一个线程来取进而实现把值交替输出。
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;
public class T13_TransferQueue {
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
TransferQueue<Character> queue = new LinkedTransferQueue<Character>();
new Thread(()->{
try {
for (char c : aI) {
System.out.print(queue.take());
queue.transfer(c);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(()->{
try {
for (char c : aC) {
queue.transfer(c);
System.out.print(queue.take());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
运行结果:
图片
5. 使用LockSupport
LockSupport是Java并发包中的一个工具类,它可以用于线程的阻塞和唤醒。
你可以把它类比成Object的wait()和notify()方法,但LockSupport是比它们更加灵活和可控的。
LockSupport提供了park()和unpark()方法:
当一个线程调用park()方法时,它会被阻塞,直到另一个线程调用该线程的unpark()方法才会被唤醒。
如果调用unpark()方法时,该线程还没有调用park()方法,则该线程调用park()方法时不会被阻塞,可以直接返回。
import java.util.concurrent.locks.LockSupport;
//Locksupport park 当前线程阻塞(停止)
//unpark(Thread t)
public class T02_00_LockSupport {
static Thread t1 = null, t2 = null;
public static void main(String[] args) throws Exception {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
t1 = new Thread(() -> {
for(char c : aI) {
System.out.print(c);
LockSupport.unpark(t2); //叫醒T2
LockSupport.park(); //T1阻塞
}
}, "t1");
t2 = new Thread(() -> {
for(char c : aC) {
LockSupport.park(); //t2阻塞
System.out.print(c);
LockSupport.unpark(t1); //叫醒t1
}
}, "t2");
t1.start();
t2.start();
}
}
运行结果:
图片
6. 使用枚举类作同步标志
创建一个枚举类ReadyToRun,利用while(true)死等和枚举类指向对象不同作标志位交替输出。
public class T03_00_cas {
enum ReadyToRun {T1, T2}
static volatile ReadyToRun r = ReadyToRun.T1;
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
new Thread(() -> {
for (char c : aI) {
while (r != ReadyToRun.T1) {}
System.out.print(c);
r = ReadyToRun.T2;
}
}, "t1").start();
new Thread(() -> {
for (char c : aC) {
while (r != ReadyToRun.T2) {}
System.out.print(c);
r = ReadyToRun.T1;
}
}, "t2").start();
}
}
运行结果:
图片
总结
好了,关于这个面试题的解法了不起暂时就想到这6种情况。
这个面试题也是一道经典的多线程面试题,如果你能将这几种情况掌握,定会另面试官刮目相看。
如果你们还有新的方法欢迎和了不起一起探讨研究,毕竟代码是死的人是活的。