你的線程安全嗎??


最近在看面經,關於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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章