文章目錄
線程通信是多線程應用開發中最常見的操作,除了wait notify方法,jdk提供了多種多樣的api可以實現線程通信。
下面通過一個簡單的題目看看jdk的多種方式實現線程通信。
題目
使用兩個線程,一個線程打印1-26,另一個打印A-Z,交替打印。
方式1:wait notify
看到題目首先想到的就是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裏面,否則會死鎖。
方式2:LockSupport
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允許精準喚醒某個線程。
方式3:Atomic
利用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可以起到同樣的效果,關鍵就是保證變量的可見性。
方式4:忙循環
上面的實現方式可用一個忙循環改造一下。
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共享變量的狀態來實現交替打印,實際上並沒有線程通信。
方式5:ReentrantLock Condition
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();
}
方式6:ReentrantLock 2 Condition
可以用兩個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();
}
方式7:BlockingQueue
要實現線程阻塞自然可以想到阻塞隊列,利用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();
}
方式8:TransferQueue
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以及其他方式來實現。