最近在復習、整理之前學習的多線程的知識,本着燃燒自己,照亮他人的想法,把自己整理的一些關於多線程的學習筆記、心得分享給大家.
博主准備把自己關於多線程的學習筆記寫成三個部分分享給大家: 基礎、實戰、測試&優化
這三個部分是一環扣一環的.
1.基礎: 多線程操作的對象必須是線程安全的,所以構建線程安全的對象是一切的基礎.這一部分講的就是如何構建線程安全的類,和一些多線程的基礎知識.
2. 實戰: 構建好了線程安全的類,我們就可以用線程/線程池,去構建我們的並發程序了,如何執行任務?如何關閉線程池?如何擴展線程池?這里都會給你答案
3. 測試&優化: 構建好的程序會不會發生死鎖? 如何優化程序? 如何知道運行的結果是否正確? 這一部分會 一 一為你解答.
好了廢話不多說,本篇博客是系列的第一篇,我們來講述一下線程安全.
線程安全
在多線程環境下,保證線程訪問的數據的安全格外重要.編寫線程安全的代碼,本質上就管理狀態的訪問,而且通常是共享的、可變的狀態.
狀態:可以理解為對象的成員變量.
共享: 是指變量可以被多個線程訪問
可變: 是指變量的值在生命周期內可以改變.
保證線程安全就是要在不可控制的並發訪問中保護數據.
如果對象在多線程環境下無法保證線程安全,就會導致臟數據和其他不可預期的后果
有很多在單線程環境下運行良好的代碼,在多線程環境下卻有問題.例如自增操作:
public class Increment { private int num = 0; public void doSomething(){ //do something
num++; } }
每次調用doSomething()方法的時候,num都會執行自增操作.但是在多線程環境下,這段代碼是有問題的.
原因在於num++並不是原子操作,而是由三個離散操作組合而來的:"讀-改-寫",讀取當前的值,加1,寫入變量.
可能會出現某一時刻,兩個線程同時讀到num的數值,然后分別+1,分別寫入.這樣其中一次計數就不存在了.
有一個專門形容這類情況的名詞,叫競爭條件
當計算的正確性依賴於運行時相關的時序或者多線程的交替時,會產生競爭條件.
我對競爭條件的理解就是,多個線程同時訪問一段代碼,因為順序的問題,可能導致結果不正確,這就是競爭條件.
"檢查-再運行",也是一種競爭條件.
public class Singleton { private Singleton singleton; private Singleton() { } public Singleton getSingleton(){ if(singleton == null){ singleton = new Singleton(); } return singleton; } }
看這個例子,我們把構造方法聲明為private的這樣就只能通過getSingleton()來獲得這個對象的實例了,先檢查這個對象是否被實例化了,如果沒有,那就實例化並返回,但是可能同一時刻兩個線程同時通過了條件判斷,這樣就產生了兩個對象的實例.
問題已經很清楚了,那么如何解決問題呢?
Java提供了synchronized(同步)關鍵字.只要是使用了synchronized關鍵字修飾的方法,就被加鎖了,synchronized鎖是互斥鎖,同一時間只能有一個線程占有鎖,其他對象想要獲得鎖只能等到占有鎖的線程釋放鎖.
我們來修改一下上面的代碼,使它們成為線程安全的:
private int num = 0; public synchronized void doSomething(){ //do something
num++; }
public synchronized void test(){ if (state){ //做一些事
}else{ // 做另外一些事
} }
好了,現在它們又是線程安全的了.
這種方式雖然很簡單,但是由於synchronized塊包住的代碼都會順序的執行,有時會導致令人無法忍受的響應速度
決定synchronized塊的大小需要權衡各種設計要求,包括安全性、簡單性和性能,其中安全性是絕對不能妥協的,而簡單性和性能又是互相影響的(將整個方法聲明為synchronized很簡單,但是響應速度不太好,將同步塊的代碼縮小,可能很麻煩,但是性能變好了).
那么在簡單性和性能之間我們要如何取舍呢? 這里有個原則: 通常簡單性與性能之間是相互牽制的,實現一個同步策略時,不要過早地為了性能而犧牲簡單性(這是對安全性潛在的妥協).
如有耗時長的操作(I/O啊,長時間的計算啊),切記不能放在鎖里,否則可能引發活躍度(死鎖)與性能(響應慢)的風險.
下面我們再看一段代碼:
1 public class Employees { 2 //程序員的等級 3 private int level; 4 //技能庫 5 public Map<String,String> skills; 6 7 //工資 8 private int sal; 9 10 public void updateSal(String multithreading){ 11 // 如果有會多線程這個技術 12 if (multithreading.equals(skills.get(multithreading))){ 13 //根據你的等級升職加薪操作.. 14 sal = level * sal; 15 }else{ 16 //如果不會多線程,學習多線程,更改等級為中級 17 skills.put(multithreading,multithreading); 18 level = 2; 19 //根據等級加薪,.. 20 updateSal(multithreading); 21 } 22 } 23 }
員工類有個方法,根據你會不會多線程技術來提高你的薪水,如果你的技能庫里有多線程技術,執行加薪操作,如果沒有會讓你學習,給你的技能庫加上這個技能,並且提高你的等級,但是在一些極端的情況下會出現問題,線程A走到17行添加完技能又沒修改等級的時候,可能有另一個線程重新調用方法,通過了12行的驗證,但是等級沒有改變,執行加薪操作的時候是按照等級的過期值執行的.
這里我們就要注意了,當不變約束涉及到多個變量的時候,要原子的更新它們.在這個方法上加鎖就又可以保證這個方法是線程安全的了.
最后給大家介紹一下原子變量atomic,使用原子變量也可以把自增操作變為原子的.
private AtomicLong num = new AtomicLong(0); public void doSomething(){ //do something
num.incrementAndGet(); }
好了關於線程安全和鎖就為大家簡單的介紹到這里,博主下一篇會更新關於安全發布對象的知識,這兩篇結合起來就可以幫助我們構建線程安全的類了.
如果大家有任何疑問或者對博主有什么建議,歡迎大家留下評論.樓主一定會盡快回復.本期分享就到這里,我們下期再見吧!