最近在看面經,關於Java的面試,老生常談的一個問提就是多線程的安全問題和線程的同步,在此我不免要從它的概念開始講述。
一、什麼是非線程安全
我相信,瀏覽這篇博客的讀者都知道什麼是進程,什麼是線程,進程與線程的區別。(如果你還不知道,那麼可以去查閱一下。)其中重要的一點區別就是:進程擁有獨立的地址空間,擁有屬於自己的在資源,而線程不擁有資源,它共享着進程的資源。所以當一個進程中的多線程併發運行時,就容易造成數據的不一致和數據污染問題。比如:
小王在某銀行存了100元,然後它要取50元,這時進程A查詢信息剩餘金錢100元,可以取錢,此時進程A修改金錢100-50,然後小王又要取100,如果進程B優先進程A獲取剩餘金錢100,那麼它可以繼續取錢,人們設想的是進程B應該在數據更改之後才能執行,但是線程是併發執行的,我們若不規定它的執行順序,那麼銀行就會虧損。
二、判斷線程是否安全的標準
下面這個函數,是線程安全的嗎?
顯然,是安全的,不管在什麼情況下,最後的執行結果都是count=1。我們看到這段代碼沒有任何狀態,它沒有任何作用域,也沒有任何引用,執行的作用範圍只存在它這條現成的局部變量中,不對其他線程產生影響,也就是說多線程間沒有共享數據,彼此間的行爲相對獨立互不影響,那麼線程就是安全的
public void test() {
int count = 0;
count = count+1;
System.out.println("count = "+count);
}
相反如果我們給這段代碼添加狀態,我們將count設爲全局變量,如果單線程運行結果是唯一的,如果多線程運行結果就不唯一了。
static int count = 0;//全局變量
public void test() {
count = count++;
System.out.println("count = "+count);
}
多線程運行結果顯示如下,可以發現於我們設想的結果完全不一樣。
綜上,我們可以發現,判斷一個程序是否存在線程安全問題的標準在於
1.是否是多線程環境
2.是否存在共享數據
3.是否有多條語句操作共享數據
三、實現線程安全的方式
針對線程不安全的存在條件,在Java中有兩種解決思路
1.讓共享資源至多被一個線程佔用——同步
2.讓線程擁有獨立資源,不去共享資源
四、同步的實現方式
1.synchronized(互斥鎖)
synchronized關鍵字用來控制線程同步,保證在多線程環境下,保證共享數據的一致性,它既可以修飾方法也可以修飾代碼塊,但修飾方法更加常用。
1.修飾方法:
實現原理:synchronized相當於一把鎖,用來鎖住對象,其他線程只有等當前線程執行完數據釋放鎖對象才能使用,否則一直處於等待狀態。
注:
- 非靜態同步方法:鎖對象默認是this
- 靜態同步方法:鎖對象默認是:類名.class(每個類都有一個class對象,而且是唯一的)
實現方法
修飾符 synchronized 返回值類型 方法名(參數列表){
}
舉例說明:
public class MyTest implements Runnable{
static int count=0;
public synchronized void test() {
count++;
System.out.println("count="+count);
}
public void run() {
test();
}
public static void main(String[] args) {
MyTest mt = new MyTest();
for(int i=0;i<10;i++) {
Thread t = new Thread(mt);
t.start();
}
}
}
運行結果顯示線程安全了!
synchronized在使用時有一個經常忽略的問題:它鎖的是括號內的對象而不是代碼,如果多線程處理的不是一個對象,那麼synchronized鎖就不起作用了,我們將上面的代碼main函數修改成如下所示,每次循環產生一個新的MyTest對象,這樣運行結果出現了錯誤。
for(int i=0;i<10;i++) {
MyTest mt = new MyTest();
Thread t = new Thread(mt);
t.start();
}
2.修飾代碼塊
實現原理:
保證同一時間只有一個線程執行代碼塊中的代碼。
實現方法:
synchronized(鎖){
// 操作共享資源的代碼
}
注:括號中的鎖可以是任何對象
舉例說明:修改上述代碼
//創建鎖對象
private static Object lock = new Object();
public void test() {
synchronized(lock){
count++;
System.out.println("count="+count);
}
}
運行結果如下:
2.Lock(可重入鎖)
synchronized雖然實現簡單,但是容易浪費資源,假設一個線程獲取了對應的鎖並執行,那麼其他線程需要用到該資源只能無窮等待,只有當該線程釋放鎖。釋放鎖有兩種情況:
1.獲取鎖的線程執行結束,自動釋放鎖
2.獲取鎖的線程執行發生異常,JVM讓線程放鎖
因此在JDK1.5中java爲我們提供了Lock,實現synchronized相同的功能,並且讓鎖有了可操作性,可以手動的獲取和釋放鎖,甚至還可以中斷獲取以及超時獲取的同步特性,但是從使用上說Lock明顯沒有synchronized使用起來方便快捷。
Lock和synchronized的區別:
1.Lock是一個接口,基於JDK層面實現,而synchronized是Java中的關鍵字,synchronized是Java內置的語言,基於JVM實現。
2.synchronized不需要手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之後,系統會自動讓線程釋放對鎖的佔用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
Lock用於獲取鎖,但它不會主動釋放鎖所以需要與unlock()配合使用。一般在使用Lock時必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。
在這裏我推薦大家去看博客可以說寫的很詳細了!https://blog.csdn.net/csdnnews/article/details/82321777#commentBox