Java基礎(32)——多線程相關知識詳解及示例分析四(線程間的互斥)
版權聲明
- 本文原創作者:清風不渡
- 博客地址:https://blog.csdn.net/WXKKang
一、線程間的互斥
1、爲什麼需要實現線程間的互斥
我們知道,線程會有多個併發運行的情況,那麼,當多個併發的線程需要使用共享數據時,我們就必須需要考慮使用共享數據的線程狀態與行爲,否則就不能保證共享數據的一致性,也就不能保證程序的正確性了
(1)共享數據
那麼,什麼是共享數據呢?很簡單,就是當這個數據需要被多個線程使用的時候,這些數據就是共享數據
如下圖所示,有Object1、Object2、Object3三個對象,線程A訪問的是Object1、Object2這兩個對象,而線程B訪問的是Object2、Object3這兩個對象,那麼,Object2即爲共享對象,Object1、Object3則不是共享對象
(2)如果線程對共享數據只有讀操作,沒有寫操作(更改操作)
既然訪問共享數據的線程都是隻有讀操作,那麼這段共享數據就不會改變,那麼這些線程之間就不會產生併發衝突,即不會產生併發互斥,因爲所有的線程每次讀取出來的共享數據的值都是相同的,這與我們所希望讀取的數據相符合
(3)如果線程對共享數據既有讀操作,也有寫操作(更改操作)
當多個線程之間有共享數據時,如果有多個線程需要讀寫共享數據,那麼線程之間就可能會產生併發衝突,導致嚴重的邏輯錯誤
由於多個線程之間沒有做任何控制,這兩個線程的調度順序是隨機的,它們的執行順序是不可預測的。由於對共享對象有寫操作來修改對象狀態,導致同一線程讀取共享對象的狀態可能前後不一致
2、示例
對於上面所說的線程對共享數據既有讀操作,也有寫操作(更改操作),我們通過一個示例來更好的學習它,現在我們模擬一個售票場景,比如現在有20張機票,需要在三個窗口同時售賣,那麼我們該如何設計這個小程序呢?
(1)無共享數據
SaleTicket類代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class SaleTicket extends Thread{
int num = 20; //票數,在每個對象裏都會維護一份數據
public SaleTicket() {
}
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while(true){
if(num > 0){
System.out.println(Thread.currentThread().getName()+"售出了第"+num+"號票");
num --;
}else{
System.out.println(Thread.currentThread().getName()+"已售完!!");
break;
}
}
}
}
測試類代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//創建SaleTicket實體類線程對象
SaleTicket saleTicket1 = new SaleTicket("窗口一");
SaleTicket saleTicket2 = new SaleTicket("窗口二");
SaleTicket saleTicket3 = new SaleTicket("窗口三");
saleTicket1.start();
saleTicket2.start();
saleTicket3.start();
}
}
執行結果如下:
由執行結果我們可以發現,20張機票被共被售賣了60次,每一個窗口都售賣了20張票,這是爲什麼呢?
因爲num是非靜態成員變量,所以在每個對象中都會維護一份成員變量,這樣三個線程總共會有三份對象,因此總和是60張
解決方案:把num票數共享出來給三個線程對象使用,方法就是使用static修飾變量num,代碼如下:
(2)有共享數據
SaleTicket類代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class SaleTicket extends Thread{
static int num = 20; //票數,在每個對象裏都會維護一份數據
public SaleTicket() {
}
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while(true){
if(num > 0){
System.out.println(Thread.currentThread().getName()+"售出了第"+num+"號票");
num --;
}else{
System.out.println(Thread.currentThread().getName()+"已售完!!");
break;
}
}
}
}
測試類代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//創建SaleTicket實體類線程對象
SaleTicket saleTicket1 = new SaleTicket("窗口一");
SaleTicket saleTicket2 = new SaleTicket("窗口二");
SaleTicket saleTicket3 = new SaleTicket("窗口三");
saleTicket1.start();
saleTicket2.start();
saleTicket3.start();
}
}
執行結果如下:
由上面的執行結果我們可以發現,雖然總體來說比沒有設置共享數據的時候誤差小了,但還是有問題的,那是什麼問題呢?就是我們之前所說過的線程安全問題,一張票被售賣了多次!!!
因爲多個線程併發執行時是無序的,並不能保證售賣票的時候不被打斷,這樣就可能發生同一張票被重複售賣,那麼我們怎麼解決這個問題呢?這就要使用到之前所講過的Java提供的互斥機制了
3、互斥實現
多個線程併發訪問共享變量、共享資源(以下統稱共享數據),容易產生線程安全問題。爲了解決該問題:在某一時刻,共享數據只能被一個線程訪問,該線程訪問結束後其他線程纔可以對其進行訪問。鎖(Lock)就是這種思路以保障線程安全的線程同步機制。一個線程在訪問共享數據時獲得鎖,在訪問結束後釋放鎖以便其他線程可以訪問該共享數據
(1)線程同步機制
在Java中我們有兩種方式實現同步:同步代碼塊、同步函數,都需要使用synchronized來定義
A、同步代碼塊
同步代碼塊的格式,需要顯示指定鎖對象。在進入代碼塊時需要獲得鎖對象,如果沒有得到鎖對象則等待;在退出代碼塊時自動釋放鎖對象,語法格式如下:
synchronized(鎖對象){
需要被同步的代碼…
}
注意:同步代碼塊中的鎖對象可以是任意對象
B、同步函數
在普通函數的返回值前面加上synchronized關鍵字,這種方式的鎖對象就是這個函數所在的對象。在進入函數時需要獲得鎖對象,如果沒有得到鎖對象則等待;在退出函數時自動釋放鎖對象,語法格式如下:
synchronized 函數返回值 函數名([參數列表]){
需要被同步的代碼…
}
注意:
1、同步函數中的鎖對象是this
2、如果該方法是靜態的同步函數那麼鎖對象是類的字節碼文件對象
C、注意事項
1、任意的一個對象都可以做爲鎖對象
2、在同步代碼塊中調用了sleep方法並不釋放鎖對象
3、只有真正存在線程安全問題的時候才使用同步代碼塊,否則會降低效率
4、多個線程操作共享數據的鎖對象必須是同一個鎖,否則加鎖無效。不理解這個,就無法理解生產者,消費者示例中需要在兩處同步代碼中加同一個鎖
(2)詳細實現
多線程互斥程序設計的重點在於確定鎖對象和互斥區域,其具體步驟如下:
A、確定鎖對象
互斥產生的原因,是由於多個線程對同一共享數據進行修改。爲了保證某個線程的修改過程不被打斷,我們通過設置互斥區域,在互斥區域中操作時將鎖定這個共享數據,其它線程都不能使用。只有這個線程修改完成後,釋放了鎖對象,其它線程纔有機會執行。因此,多個線程一定需要共享鎖對象。 下 面例子中使用一個獨立的Object對象作爲鎖對象,並將它定義爲static,這樣將被所有線程實例共享
B、確定互斥區域
通常,互斥區域越小,性能就越好,下面例子中使用互斥代碼塊實現
在SaleTicket類中封裝了數據和相應的操作,只需要在互斥代碼塊中包括寫共享數據的代碼即可
C、示例代碼
像上面示例中的問題如果需要使用互斥機制來解決的話該怎麼辦呢?代碼如下:
SaleTicket類代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class SaleTicket extends Thread{
private static int num = 20; //票數,在每個對象裏都會維護一份數據
private static Object lock = new Object();
public SaleTicket() {
}
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
synchronized (lock) {
//同步代碼塊
while(true){ //lock是鎖對象
if(num > 0){
System.out.println(Thread.currentThread().getName()+"售出了第"+num+"號票");
num --;
}else{
System.out.println(Thread.currentThread().getName()+"已售完!!");
break;
}
}
}
}
}
測試類代碼如下:
package qfbd.com;
/*
原創作者:清風不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//創建SaleTicket實體類線程對象
SaleTicket saleTicket1 = new SaleTicket("窗口一");
SaleTicket saleTicket2 = new SaleTicket("窗口二");
SaleTicket saleTicket3 = new SaleTicket("窗口三");
saleTicket1.start();
saleTicket2.start();
saleTicket3.start();
}
}
這樣我們就實現了通過線程互斥機制來保證線程併發時數據的安全性,如果效果不明顯可將票數調大一點即可