概述
最近閱讀了《Java高併發實戰》一書,也瞭解了一些多線程方面的知識,但是一直沒有嘗試過寫Coding。畢竟紙上得來終覺淺,因此通過本篇文章,對多個線程輪流打印、死鎖、讀寫鎖的實現問題進行總結,算是對多線程的一種鞏固。主要涉及到的知識點就是synchronized
鎖和wait
、notify
線程通信機制。
線程輪流打印
問題描述
給定三個線程,代碼的邏輯順序是A->B->C
,每個線程內分別打印一條“This is x”
語句,如何做到最終打印順序是C->B->A
。
問題思考
要把多個線程的執行順序給安排上,聽起來就感覺要加鎖,某個線程加鎖執行完成,釋放鎖再進行下一個線程的執行,這隻能保證可以以一定的順序執行而不會亂序,但是具體的順序就需要引入一個變量,用於標識當前應該由幾號線程進行打印。在每個線程內去不斷輪詢是否輪到自己打印,如果沒有輪到自己打印就wait進入阻塞狀態,當別的線程打印完後,自己被喚醒繼續去輪詢是否輪到自己打印,當輪到自己時候,打印完成修改變量,並喚醒其它的所有線程,讓其它的線程去判斷是否輪到自己打印。整個邏輯就是如此,如果需要輪流打印C->B->A
多次,使用for
循環調用orderThread.printX
即可。
具體實現
public class OrderThread {
private int orderNum = 3;
public static void main(String[] args) {
OrderThread orderThread = new OrderThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
orderThread.printA();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
orderThread.printB();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
orderThread.printC();
}
});
t1.start();
t2.start();
t3.start();
}
public synchronized void printA(){
while (orderNum != 1) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 3;
System.out.println("This is A");
notifyAll();
}
public synchronized void printB() {
while (orderNum != 2) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 1;
System.out.println("This is B");
notifyAll();
}
public synchronized void printC() {
while (orderNum != 3) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 2;
System.out.println("This is C");
notifyAll();
}
}
運行結果
This is C
This is B
This is A
線程輪流打印-拓展
問題描述
給定三個線程,代碼的邏輯順序是A->B->C
,三個線程按照C->B->A
的順序輪流打印1-100
。
問題思考
該問題是“線程輪流打印”問題的拓展,依然是三個線程輪流打印,只是打印的內容進行了改變,需要從1開始計數打印。這就需要我們設置一個全局變量,每個線程內都對該全局變量進行打印並進行加一操作,以便下一個線程能夠打印出正確的數字。需要注意的是:在while
循環和線程執行體內都需要對待打印的變量num
進行判斷。while
循環中的num判斷是爲了多次調用print
方法,線程執行體內的num
判斷是爲了避免在調用print
方法時num
尚未達到100,但是print
方法執行過程中num
已經超出100的情況,如果取消掉線程執行體內的num
判斷,程序會打印1-102。
具體實現
public class ThreadPrint {
private int orderNum = 3;
public static int num = 1;
public static void main(String[] args) {
ThreadPrint orderThread = new ThreadPrint();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (num <= 100) {
orderThread.printA();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (num <= 100) {
orderThread.printB();
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
while (num <= 100) {
orderThread.printC();
}
}
});
t1.start();
t2.start();
t3.start();
}
public synchronized void printA(){
while (orderNum != 1) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 3;
if (num <= 100) {
System.out.println("This is A " + num);
}
num++;
notifyAll();
}
public synchronized void printB() {
while (orderNum != 2) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 1;
if (num <= 100) {
System.out.println("This is B " + num);
}
num++;
notifyAll();
}
public synchronized void printC() {
while (orderNum != 3) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 2;
if (num <= 100) {
System.out.println("This is C " + num);
}
num++;
notifyAll();
}
}
死鎖
問題描述
給定兩個線程,使得程序產生死鎖。
問題思考
既然需要產生死鎖,就必須考慮到死鎖產生的條件。
我們來逐條分析一下死鎖的產生條件:
1、互斥,要做到資源互斥,只需要給資源加個鎖,每個時刻就只能有一個線程能夠獲取到該資源;
2、不可剝奪,一般對於正常的程序而言,除非發生中斷或者程序故障,否則對於線程已獲得的資源,在未使用完成之前都是不可剝奪的,只能在使用後自己釋放;
3、請求和保持,意味着當前線程需要持有一個資源,並去請求另一個資源;
4、循環等待,由上述條件進行引申,意味着A線程持有資源1並請求資源2,B線程持有資源2並請求資源1,形成環路。
綜上,我們只需要創建兩個不可剝奪的資源,並在不同的線程中通過synchronized
關鍵字,根據不同的順序對資源進行持有,這樣即可實現死鎖。
具體實現
public class DeadTest {
public static void main(String[] args) {
DeadLock deadLock1 = new DeadLock(true);
DeadLock deadLock2 = new DeadLock(false);
Thread t1 = new Thread(deadLock1);
Thread t2 = new Thread(deadLock2);
t1.start();
t2.start();
}
}
class DeadLock implements Runnable {
//用於標識兩個線程
private boolean flag;
//兩個資源
static final Object obj1 = new Object();
static final Object obj2 = new Object();
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
//保持1並請求2
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + "持有obj1");
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + "持有obj2");
}
}
}
} else {
while (true) {
//保持2並請求1
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + "持有obj2");
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + "持有obj1");
}
}
}
}
}
}
讀寫鎖
問題描述
實現一個讀寫鎖。讀鎖可以在沒有寫鎖時被多個線程同時持有,寫鎖是獨佔的,每次只能有一個寫線程,但是可以有多個線程併發地讀數據。
問題思考
如果我們讀寫共用一把鎖,那麼實際上整個程序的讀寫就是串行的,同一時刻只能有一個線程對數據進行讀或寫,效率顯然比較低,讀寫鎖比互斥鎖允許對於共享數據更大程度的併發。讀寫鎖主要是爲了能夠將讀寫分離,因爲程序其實是允許同一時刻,多個線程同時讀取數據的,只要在讀的期間沒有寫操作,即可保證所有讀線程都能讀到一致的數據。因此我們需要考慮到讀寫鎖的原則:
1、讀讀能共存;
2、讀寫不能共存;
3、寫寫不能共存。
那麼對於讀鎖而言,如果當前有線程在進行寫入操作,則進入等待狀態,直到所有的寫鎖釋放即可獲得讀鎖;對於寫鎖而言,只要當前沒有寫鎖存在,則優先預搶佔到寫鎖,避免被其它線程搶佔,是一種先來先服務的實現,但是此時還沒有真正獲得寫鎖,直到判斷當前不存在讀鎖,才能真正獲取到寫鎖進行數據的寫入。無論對於讀鎖還是寫鎖而言,鎖的釋放直接對鎖資源進行更新,然後通知其它所有線程即可。
具體實現
public class ReadWriteLockTest {
public static void main(String[] args) {
ReadWriteLockDemo readWriteLock = new ReadWriteLockDemo();
//啓動寫線程
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.write(1);
}
}, "Write1").start();
//啓動10個讀線程
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.read();
}
}).start();
}
//啓動寫線程
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.write(2);
}
}, "Write2").start();
}
}
class ReadWriteLockDemo {
private ReadWriteLock readWriteLock = new ReadWriteLock();
private int num = 0; //共享資源
//讀
public void read() {
try {
readWriteLock.lockRead();
System.out.println(Thread.currentThread().getName() + " read " + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.unlockRead();
}
}
//寫
public void write(int number) {
try {
readWriteLock.lockWrite();
this.num = number;
System.out.println(Thread.currentThread().getName() + " write " + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.unlockWrite();
}
}
}
class ReadWriteLock {
private int readLock = 0;
private int writeLock = 0;
public synchronized void lockRead() throws Exception{
while (writeLock > 0) {
wait();
}
readLock++;
}
public synchronized void unlockRead() {
readLock--;
notifyAll();
}
public synchronized void lockWrite() throws Exception{
while (writeLock > 0) {
wait();
}
writeLock++;
while (readLock > 0) {
wait();
}
}
public synchronized void unlockWrite() {
writeLock--;
notifyAll();
}
}
運行結果
Write1 write 1
Thread-1 read 1
Thread-2 read 1
Thread-5 read 1
Thread-0 read 1
Thread-3 read 1
Thread-4 read 1
Thread-8 read 1
Thread-6 read 1
Thread-9 read 1
Thread-7 read 1
Write2 write 2
從程序的運行結果中可以看出,對於讀操作而言,只有在寫操作結束後才能進行,並且不能保證線程執行的先後順序,因爲讀操作並不是互斥的;而對於寫操作而言,在讀鎖和寫鎖全部釋放之後才能進行。因此,Read
線程必須在Write1
線程完成並釋放寫鎖後才能執行,並且所有Read
線程的執行是無序的,Write2
線程只能等待所有Read
線程完成並釋放讀鎖後才能執行。