線程安全問題:
線程安全出現的根本原因:
1.存在兩個或者兩個以上的線程對象共享同一個資源;
2.多線程操作共享資源代碼有多個語句。
一、使用同步代碼塊
如:賣票案例 出現了線程安全 重復的票不能出現
步驟:成員位置建立鎖對象;
synchronized(鎖對象){ 出現安全問題代碼 }
注意事項:
1 鎖對象可以是任意對象
2 必須保證多個線程使用的是同一個鎖對象 、
3 把{} 讓一個線程進
4.一個線程在同步代碼塊中sleep了,並不會釋放鎖對象;
5.如果不存在線程安全問題,千萬不要使用同步代碼塊;
6.鎖對象必須是多線程共享的一個資源,否則鎖不住。
例子:
public class RunnableImpl implements Runnable{ // 定義共享資源 線程不安全 private int ticket = 100; //在成員位置創建一個鎖對象 Object obj = new Object(); // 線程任務 賣票 @Override public void run() { while(true){ //建立鎖對象 synchronized (obj){ if(ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //賣票操作 System.out.println(Thread.currentThread().getName()+"正在賣第"+ticket+"張票"); ticket--; } } } } }
二、使用同步方法(函數)解決多線程安全
同步函數就是使用synchronized修飾一個函數
步驟
1 創建一個方法 修飾符添加synchronized 2 把訪問了 共享數據的代碼放入到方法中 3 調用同步方法
注意事項:
同步函數注意事項: 1.如果函數是一個非靜態的同步函數,那么鎖對象是this對象; 2.如果函數是靜態的同步函數,那么鎖對象是當前函數所屬的類的字節碼文件(class對象); 3.同步函數的鎖對象是固定的,不能由自己指定。
例子:
public class RunnableImpl implements Runnable{ // 定義共享資源 線程不安全 private int ticket = 100; // 線程任務 賣票 @Override public void run() { while(true){ payTicket();//調用下面synchronized修飾的方法 } } public synchronized void payTicket(){ if(ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //賣票操作 System.out.println(Thread.currentThread().getName()+"正在賣第"+ticket+"張票"); ticket--; } } }
推薦使用:同步代碼塊 原因: 1.同步代碼塊的鎖對象可以由我們自由指定,方便控制; 2.同步代碼塊可以方便的控制需要被同步代碼的范圍,同步函數必須同步函數的所有代碼。
三.使用lock鎖
原因:synchronized的缺陷
synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性。那么為什么會出現Lock呢?
synchronized 的局限性 與 Lock 的優點 如果一個代碼塊被synchronized關鍵字修飾,當一個線程獲取了對應的鎖,並執行該代碼塊時,其他線程便只能一直等待直至占有鎖的線程釋放鎖。事實上,占有鎖的線程釋放鎖一般會是以下三種情況之一: 1:占有鎖的線程執行完了該代碼塊,然后釋放對鎖的占有; 2:占有鎖線程執行發生異常,此時JVM會讓線程自動釋放鎖; 3:占有鎖線程進入 WAITING 狀態從而釋放鎖,例如在該線程中調用wait()方法等。 試考慮以下三種情況: Case 1 : 在使用synchronized關鍵字的情形下,假如占有鎖的線程由於要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,那么其他線程就只能一直等待,別無他法。
這會極大影響程序執行效率。因此,就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間 (解決方案:tryLock(long time, TimeUnit unit))
或者 能夠響應中斷 (解決方案:lockInterruptibly())),這種情況可以通過 Lock 解決。 Case 2 : 我們知道,當多個線程讀寫文件時,讀操作和寫操作會發生沖突現象,寫操作和寫操作也會發生沖突現象,但是讀操作和讀操作不會發生沖突現象。
但是如果采用synchronized關鍵字實現同步的話,就會導致一個問題,即當多個線程都只是進行讀操作時,也只有一個線程在可以進行讀操作,其他線程只能等待鎖的釋放而無法進行讀操作。
因此,需要一種機制來使得當多個線程都只是進行讀操作時,線程之間不會發生沖突。同樣地,Lock也可以解決這種情況 (解決方案:ReentrantReadWriteLock) 。 Case 3 : 我們可以通過Lock得知線程有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized無法辦到的。 上面提到的三種情形,我們都可以通過Lock來解決,但 synchronized 關鍵字卻無能為力。
事實上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 實現提供了比 synchronized 關鍵字 更廣泛的鎖操作,它能以更優雅的方式處理線程同步問題。也就是說,Lock提供了比synchronized更多的功能。
總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點: 1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問; 2)Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;
而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
Lock接口實現類的使用
Lock接口有6個方法:
// 獲取鎖 void lock() // 如果當前線程未被中斷,則獲取鎖,可以響應中斷 void lockInterruptibly() // 返回綁定到此 Lock 實例的新 Condition 實例 Condition newCondition() // 僅在調用時鎖為空閑狀態才獲取該鎖,可以響應中斷 boolean tryLock() // 如果鎖在給定的等待時間內空閑,並且當前線程未被中斷,則獲取鎖 boolean tryLock(long time, TimeUnit unit) // 釋放鎖 void unlock()
注意
lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用來獲取鎖的。
unLock()方法是用來釋放鎖的。newCondition() 返回 綁定到此 Lock 的新的 Condition 實例 ,用於線程間的協作,詳細內容請查找關鍵詞:線程間通信與協作。
參考: https://www.cnblogs.com/myseries/p/10784076.html
https://www.bilibili.com/video/BV1uJ411k7wy?p=323