JAVA多線程線程安全性基礎
線程安全性
一個對象是否需要是線程安全的,取決於它是否被多個線程訪問,而不取決於對象要實現的功能
什麼是線程安全的代碼
核心:對 共享的 和 可變的 狀態的訪問進行管理。防止對數據發生不受控的並發訪問。
何為對象的狀態?
狀態是指存儲在對象的狀態變量(例如實例或靜態域)中的數據。還可能包括 其他依賴對象 的域。
eg:某個HashMap的狀態不僅存儲在HashMap對象本身,還存儲在許多Map.Entry對象中。
總而言之,在對象的狀態中包含瞭任何可能影響其外部可見行為的數據。
何為共享的?
共享的 是指變量可同時被多個線程訪問
何為可變的?
可變的 是指變量的值在其生命周期內可以發生變化。試想,如果一個共享變量的值在其生命周期內不會發生變化,那麼在多個
線程訪問它的時候,就不會出現數據不一致的現象,自然就不存在線程安全性問題瞭。
什麼是線程安全性
當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,達到預期的效果,那麼就稱這個類是線程安全的。
如下啟動10個線程,每個線程對inc執行1000次遞增,並添加一個計時線程,預期效果應為10000,而實際輸出值為6880,是一個小於10000的值,並未達到預期效果,因此INS類不是線程安全的,整個程序也不是線程安全的。原因是遞增操作不是原子操作,並且沒有適當的同步機制
package hgh0808; public class Test { public static void main(String[] args){ for(int i = 0;i < 10;i++){ Thread th = new Thread(new CThread()); th.start(); } TimeThread tt = new TimeThread(); tt.start(); try{ Thread.sleep(21000); }catch(Exception e){ e.printStackTrace(); } System.out.println(INS.inc); } } --------------------------------------------------------------------- package hgh0808; import java.util.concurrent.atomic.*; public class TimeThread extends Thread{ @Override public void run(){ int count = 1; for(int i = 0;i < 20;i++){ try{ Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } System.out.println(count++); } } } --------------------------------------------------------------------- package hgh0808; public class CThread implements Runnable{ @Override public void run(){ for(int j = 0;j < 1000;j++){ INS.increase(); } } } --------------------------------------------------------------------- package hgh0808; public class INS{ public static volatile int inc = 0; public static void increase(){ inc++; } } =====================================================================
執行結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
6880
通過synchronized加鎖機制,對INS類實現同步,如下得到瞭正確的運行結果,很容易可以看出,主調代碼中並沒有任何額外的同步或協同,此時的INS類是線程安全的,整個程序也是線程安全的
package hgh0808; public class INS{ public static volatile int inc = 0; public static void increase(){ synchronized (INS.class){ inc++; } } }
執行結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000
如何編寫線程安全的代碼
————————————————————————————————
如果當多個線程訪問同一個可變的狀態變量時沒有使用合適的同步,那麼程序就會出現錯誤,像上文中進行同步之前的代碼
有三種方式可以修復這個問題:
*不在線程之間共享該狀態變量
*將狀態變量修改為不可變的變量
*在訪問狀態變量時使用同步
前兩種方法是針對 共享 和 不變 這兩個屬性(見上文)解決問題,在有些情境下會違背程序設計的初衷(比如上文中INS類中的inc變量不可能不變,且在多核處理器的環境下為瞭提高程序性能,就需要多個線程同時處理,這樣變量就必然要被多個線程共享)。
基於此,我們針對第三種方式—— 在訪問狀態變量時使用同步 展開討論
在討論第三種方式之前,我們先介紹幾個簡單的概念
原子性 :一個操作序列的所有操作要麼不間斷地全部被執行,要麼一個也沒有執行
競態條件 :當某個計算的正確性取決於多個線程的的交替執行時序時,就會發生競態條件。通俗的說,就是某個程序結果的正確性取決於運氣時,就會發生競態條件。(競態條件並不總是會產生錯誤,還需要某種不恰當的執行時序)
常見的競態條件類型:
*檢查–執行(例如延遲初始化)
*讀取–修改–寫入(例如自增++操作)
針對以上兩種常見的競態條件類型,我們分別給出例子
延遲初始化(檢查--執行) -------------------------------------------------------------------- package hgh0808; import java.util.ArrayList; public class Test1 { public ArrayList<Ball> list; public ArrayList<Ball> getInstance(){ if(list == null){ list = new ArrayList<Ball>(); } return list; } } class Ball{ }
大概邏輯是先判斷list是否為空,若為空,創建一個新的ArrayList對象,若不為空,則直接使用已存在的ArrayList對象,這樣可以保證在整個項目中list始終指向同一個對象。這在單線程環境中是完全沒有問題的,但是如果在多線程環境中,list還未實例化時,A線程和B線程同時執行if語句,A和B線程都會認為list為null,A和B線程都會執行實例化語句,造成混亂。
自增++操作(讀取--修改--寫入) ------------------------------------------------------------------------ 參考上文中為改進之前的代碼(對INS類中inc的自增)
以上兩個例子告訴我們,必須添加適當的同步策略,保證復合操作的原子性,防止競態條件的出現
策略一:使用原子變量類,在java.util.concurrent.atomic包中包含瞭一些原子變量類
package hgh0808; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; public class INS{ public static AtomicInteger inc = new AtomicInteger(0); public static void increase(){ inc.incrementAndGet(); } }
執行結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000
值得註意的是,隻有一個狀態變量時,可以通過原子變量類實現線程安全。但是如果有多個狀態變量呢?
設想一個情景
多個線程不斷產生1到10000的隨機數並且發送到一個計算線程,計算線程每獲取一個數字n,就計算sinx在[0,n]上的積分並打印到控制臺上,為瞭提高程序性能,設計一個緩存機制,保存上次的數字n和積分結果(兩個狀態變量)。如果本次的數字和上次的數字相等,直接打印積分結果,避免重復計算。
看代碼:
package hgh0808; import java.util.concurrent.atomic.AtomicReference; public class Calculate extends Thread{ private final AtomicReference<Double> lastNumber = new AtomicReference<Double>(); //緩存機制,原子變量類 private final AtomicReference<Double> lastRes = new AtomicReference<Double>(); //緩存機制,原子變量類 private static final double N = 100000; //將區間[0,e]分成100000份,方便定積分運算 public void service() throws Exception{ getData(); Thread.sleep(10000); //等待MyQueue隊列中有一定數量的元素後,再開始從其中取元素 while(true){ Double e; if(!MyQueue.myIsEmpty()){ e = MyQueue.myRemove(); }else{ return; } if(e.equals(lastNumber.get())){ System.out.println(lastNumber.get()+" "+lastRes.get()); }else{ Double temp = integral(e); lastNumber.set(e); lastRes.set(temp); System.out.println(e+" "+temp); } Thread.sleep(2000); } } public void getData(){ //創建並啟動四個獲取隨機數的線程,這四個線程交替向MyQueue隊列中添加元素 Thread1 th1 = new Thread1(); Thread2 th2 = new Thread2(); Thread3 th3 = new Thread3(); Thread4 th4 = new Thread4(); th1.start(); th2.start(); th3.start(); th4.start(); } public Double integral(double e){ //計算定積分 double step = (e-0)/N; double left = 0,right = step; double sum = 0; while(right <= e){ double mid = left+(right-left)/2; sum+=Math.sin(mid); left+=step; right+=step; } sum*=step; return sum; } } --------------------------------------------------------------------- package hgh0808; import java.util.LinkedList; public class MyQueue { //由於LinkedList是線程不安全的,因此需要將其改寫為線程安全類 private static LinkedList<Double> queue = new LinkedList<>(); synchronized public static void myAdd(Double e){ queue.addLast(e); } synchronized public static void myClear(){ queue.clear(); } synchronized public static int mySize(){ return queue.size(); } synchronized public static boolean myIsEmpty(){ return queue.isEmpty(); } synchronized public static double myRemove(){ return queue.removeFirst(); } } ----------------------------------------------------------------------- package hgh0808; import java.util.Random; public class Thread1 extends Thread{ private double data; @Override public void run(){ while(true){ Random random = new Random(); data = (double) (random.nextInt(10000)+1); if(MyQueue.mySize() > 10000){ //由於從隊列中取元素的速度低於四個線程向隊列中加元素的速度,因此隊列的長度是趨於擴張的,當達到一定程度時,清空隊列 MyQueue.myClear(); } MyQueue.myAdd(data); try { Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } } ------------------------------------------------------------------------ package hgh0808; import java.util.Random; public class Thread2 extends Thread{ private double data; @Override public void run(){ while(true){ Random random = new Random(); data = (double) (random.nextInt(10000)+1); if(MyQueue.mySize() > 10000){ MyQueue.myClear(); } MyQueue.myAdd(data); try { Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } } ----------------------------------------------------------------------- package hgh0808; import java.util.Random; public class Thread3 extends Thread{ private double data; @Override public void run(){ while(true){ Random random = new Random(); data = (double) (random.nextInt(10000)+1); if(MyQueue.mySize() > 10000){ MyQueue.myClear(); } MyQueue.myAdd(data); try { Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } } ------------------------------------------------------------------------ package hgh0808; import java.util.Random; public class Thread4 extends Thread{ private double data; @Override public void run(){ while(true){ Random random = new Random(); data = (double) (random.nextInt(10000)+1); if(MyQueue.mySize() > 10000){ MyQueue.myClear(); } MyQueue.myAdd(data); try { Thread.sleep(1000); }catch(Exception e){ e.printStackTrace(); } } } }
隻看Calculate線程,不看其他線程和MyQueue中的鎖機制,本問題的焦點在於Calculate線程中對多個狀態變量的同步策略
存在問題:
盡管對lastNumber和lastRes的set方法的每次調用都是原子的,但仍然無法同時更新lastNumber和lastRes;如果隻修改瞭其中一個變量,那麼在這兩次修改操作之間,其它線程將發現不變性條件被破壞瞭。換句話說,就是沒有足夠的原子性
**當在不變性條件中涉及多個變量時,各個變量間並不是彼此獨立的,而是某個變量的值會對其它變量的值產生約束。因此當更新某一個變量時,需要在同一個原子操作中對其他變量同時進行更新。
改進 ================>加鎖機制 內置鎖 synchronized
之所以每個對象都有一個內置鎖,隻是為瞭免去顯式地創建鎖對象
synchronized修飾方法就是橫跨整個方法體的同步代碼塊
非靜態方法的鎖—–方法調用所在的對象
靜態方法的鎖—–方法所在類的class對象
public class Calculate extends Thread{ private final AtomicReference<Double> lastNumber = new AtomicReference<Double>(); //緩存機制,原子變量類 private final AtomicReference<Double> lastRes = new AtomicReference<Double>(); //緩存機制,原子變量類 private static final double N = 100000; //將區間[0,e]分成100000份,方便定積分運算 public void service() throws Exception{ getData(); Thread.sleep(10000); //等待MyQueue隊列中有一定數量的元素後,再開始從其中取元素 while(true){ Double e; synchronized (this){ //檢查--執行 使用synchronized同步,防止出現競態條件 if(!MyQueue.myIsEmpty()){ e = MyQueue.myRemove(); }else{ return; } } if(e.equals(lastNumber.get())){ System.out.println(lastNumber.get()+" "+lastRes.get()); }else{ Double temp = integral(e); synchronized (this) { //兩個狀態變量在同一個原子操作中更新 lastNumber.set(e); lastRes.set(temp); } System.out.println(e+" "+temp); } Thread.sleep(2000); } } public void getData(){ //創建並啟動四個獲取隨機數的線程,這四個線程交替向MyQueue隊列中添加元素 Thread1 th1 = new Thread1(); Thread2 th2 = new Thread2(); Thread3 th3 = new Thread3(); Thread4 th4 = new Thread4(); th1.start(); th2.start(); th3.start(); th4.start(); } public Double integral(double e){ //計算定積分 double step = (e-0)/N; double left = 0,right = step; double sum = 0; while(right <= e){ double mid = left+(right-left)/2; sum+=Math.sin(mid); left+=step; right+=step; } sum*=step; return sum; } }
對於包含多個變量的不變性條件中,其中涉及的所有變量都需要由同一個鎖來保護
synchronized (this) { //兩個狀態變量在同一個原子操作中更新 lastNumber.set(e); lastRes.set(temp); }
鎖的重入
如果某個線程試圖獲得一個已經由它自己持有的鎖,那麼這個請求就會成功,“重入”意味著獲取鎖的操作的粒度是‘線程’,而不是‘調用’。
重入的一種實現方式 :
為每個鎖關聯一個獲取計數值和一個所有者線程。當計數值為0時,這個鎖就被認為是沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且將獲取計數值置為1。如果同一個線程再次獲取這個鎖,計數值將遞增,當線程退出同步代碼塊時,計數器會相應地遞減。當計數值為0時,這個鎖將被釋放。
如果內置鎖不可重入,那麼以下這段代碼將發生死鎖(每個doSomething方法在執行前都會獲取Father上的內置鎖) ---------------------------------------------------------------------- public class Father{ public synchronized void doSomething(){ } } public class Son extends Father{ @Override public synchronized void doSomething(){ System.out.println("重寫"); super.doSomething(); } }
線程安全性與性能和活躍性之間的平衡
活躍性:是否會發生死鎖饑餓等現象
性能:線程的並發度
不良並發的應用程序:可同時調用的線程數量,不僅受到可用處理資源的限制,還受到應用程序本身結構的限制。幸運的是,通過縮小同步代碼塊的作用范圍,可以平衡這個問題。
縮小作用范圍的原則====>當執行時間較長的計算或者可能無法快速完成的操作時,一定不能持有鎖!!!
總結
本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!
推薦閱讀:
- Java多線程實現簡易微信發紅包的方法實例
- java中synchronized關鍵字的3種寫法實例
- Java並發編程多線程間的同步控制和通信詳解
- 徹底搞懂Java多線程(二)
- 淺談Java並發中ReentrantLock鎖應該怎麼用