1. 程式人生 > >執行緒安全的實現方式和鎖優化

執行緒安全的實現方式和鎖優化

什麼是執行緒安全?

在Java中執行緒安全的場景有哪些?

不可變

絕對執行緒安全

相對執行緒安全

執行緒相容

執行緒對立

 Java中保證執行緒安全的方式?

互斥同步

非阻塞同步

無同步方案

1. 可重入程式碼

2. 執行緒本地儲存

鎖優化

自旋鎖

鎖消除

鎖粗化

輕量鎖

偏向鎖


什麼是執行緒安全?

《Java Concurrency in Practice》作者Goetz 對於執行緒安全的定義:

當多個執行緒訪問一個物件,如果不用考慮這些執行緒在執行環境下的排程和交替執行,也不需要額外的同步,或者呼叫方在進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那麼這個物件就是執行緒安全的。

對於上面的定義,需要說明的是物件內部都會封裝了保證執行緒安全的實現,但是這樣也不容易是執行緒安全的,通常我們對上面的定義弱化一點,那就是限於物件行為的單次呼叫。因為就是執行緒安全的類,多次呼叫也可能執行緒不安全,需要另外同步處理。 

在Java中執行緒安全的場景有哪些?

這裡的執行緒安全,是多個執行緒訪問共享變數這個前提,Java中多執行緒訪問共享資源的安全性由強到弱,分為下面五類。

不可變

只要一個final變數被正確構建出來(沒有發生this逃逸問題),對於其他執行緒而言,是不可變的,是執行緒安全的。

如果共享資料是基本型別,只要定義為final型別就可保證不可變,如果共享變數是物件的話,不可變指的是行為不可變,定義屬性為final型別。

絕對執行緒安全

絕對執行緒安全,從執行緒安全的定義上來看,不需要另外加上同步控制,實際上,就算是執行緒安全的類,在呼叫的時候,可能是執行緒不安全的,需要另外加上同步控制,保證是絕對安全。

package net.lingala.zip4j.test;

import java.util.Vector;

public class AbsoluteThreadSafety {
	private static Vector<Integer> vector = new Vector<>();
	public static void main(String[] args) {
		while(true) {
			for(int i=0;i<20;i++) {
				vector.add(i);
			}
			Thread remove = new Thread(new Runnable() {
				
				@Override
				public void run() {
					for(int i=0;i<vector.size();i++) {
						try {
							vector.remove(i);
						}catch(Exception e) {
							e.printStackTrace();
						}
					}
				}
			});
			Thread get = new Thread(new Runnable() {
				@Override
				public void run() {
					for(int i=0;i<vector.size();i++) {
						try {
							int m = vector.get(i);
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				}
			});
			remove.start();
			get.start();
			if(Thread.activeCount()>500) break;
		}
		
	}
}

執行結果:

java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 24
	at java.util.Vector.get(Vector.java:744)
	at net.lingala.zip4j.test.AbsoluteThreadSafety$2.run(AbsoluteThreadSafety.java:26)
	at java.lang.Thread.run(Thread.java:745)

 分析: size,remove,get方法都是synchronize修飾的同步方法,執行緒獲取沒有修改前的下標,操作時候下標發生改變,很容易發生越界。

解決:將remove,get執行緒,加上同步鎖,排隊執行。

package net.lingala.zip4j.test;

import java.util.Vector;

public class AbsoluteThreadSafety {
	private static Vector<Integer> vector = new Vector<>();
	private static Object obj = new Object();
	public static void main(String[] args) {
		while(true) {
			for(int i=0;i<20;i++) {
				vector.add(i);
			}
			Thread remove = new Thread(new Runnable() {
				
				@Override
				public void run() {
					synchronized (obj) {
						for(int i=0;i<vector.size();i++) {
							try {
								vector.remove(i);
							}catch(Exception e) {
								e.printStackTrace();
							}
						}
					}
					
				}
			});
			Thread get = new Thread(new Runnable() {
				@Override
				public void run() {
					synchronized (obj) {
						for(int i=0;i<vector.size();i++) {
							try {
								int m = vector.get(i);
							} catch (Exception e) {
								e.printStackTrace();
							}
						}
					}
					
				}
			});
			remove.start();
			get.start();
			if(Thread.activeCount()>500) break;
		}
		
	}
}

相對執行緒安全

相對執行緒安全,單獨呼叫物件的方法是安全的,不需要另外的同步操作。怎麼理解?也就是,物件對於自己的方法內部做了同步控,是執行緒安全的類,如Vector等等。

執行緒相容

物件是執行緒不安全的,但是呼叫的時候,可以加上同步控制保證執行緒安全,如ArrayList,HashMap等等。

執行緒對立

不管使用什麼措施,在多執行緒環境就是執行緒不安全的。

這種程式碼很少出現,一個例子就是Thread的suspend()和resume(),多執行緒呼叫一個執行緒例項的這兩個方法會發生死鎖。

 Java中保證執行緒安全的方式?

互斥同步

同步指的是多個執行緒訪問共享資料時,保證共享資料同一時刻只能被一個執行緒使用。互斥是同步的實現手段,互斥的實現方式有臨界區,互斥量,訊號量。比較常用的是手段是synchronize關鍵字和lock鎖。

非阻塞同步

不通過加鎖的方式,而是通過樂觀鎖的機制(CAS)實現同步。常見的手段是Atomic類

CAS存在ABA問題, 就是一個變數被修改後,又被還原了,另一個執行緒認為是沒有改過,Java中提供了一個類AtomicStampedReference來控制變數的版本解決這個問題,不過,大部分情況下,ABA問題不會影響併發的正確性。

無同步方案

1. 可重入程式碼

可重入:一段程式碼執行一部分後,中斷,轉而執行另一段程式碼,獲得控制權後返回,執行不會有任何錯誤

怎麼判斷是否是可重入的?

一段程式碼不涉及到對共享資源的讀寫操作,執行結果是可預期的,對於每個執行緒而言執行結果都是一樣的,就是可重入的

因此是執行緒安全的,例如類中沒有讀寫例項變數或者靜態變數的方法。

2. 執行緒本地儲存

將讀寫的資源和每個執行緒進行繫結,每個執行緒訪問的都是自己的資源,也就不存線上程安全問題,使用類ThreadLocal實現。

鎖優化

jdk1.6之後提供了自旋鎖、自適應自旋鎖、輕量鎖、偏向鎖、鎖粗化等手段來減少執行緒共享資料時候的開銷。

鎖存在四種狀態:無鎖狀態、偏向鎖狀態、輕量鎖狀態、重量鎖狀態,這幾種鎖的狀態是隨著競爭加強而不斷升級的。

注意,這幾種狀態,只能升級不能降級。

自旋鎖

在採用互斥同步的手段保證執行緒安全的時候,沒有獲取到鎖的執行緒只能阻塞,等待獲取到鎖後,再次執行。這種掛起,又再次執行給系統較大的開銷。並且因為實際上鎖獲取、鎖釋放的過程耗費時間不會太長,可以讓請求鎖的執行緒不放棄CPU的執行時間,執行一個忙迴圈,而不是掛起,和獲取到鎖的執行緒並行執行。

定義:請求鎖的時候,如果沒有拿到鎖,不阻塞,不放棄CPU執行時間,執行一個忙迴圈的手段就是自旋。

開啟:-XX:+UseSpinning引數開啟,預設是開啟的。

缺點:如果鎖佔用的時間較短,自旋能夠減少很多開銷,但是如果鎖佔用時間很長,自旋白白佔有CPU時間,耗費資源,因此都會限制自旋的次數,預設是10次(-XX:PreBlockPin引數可修改)

自適應自旋鎖:自旋的時間不再是一個固定值,而是由上次獲取到同一個鎖自旋的時間和鎖的狀態決定。如果同一個鎖物件,自旋等待成功獲取到鎖,認為再次自旋是可能再次成功的,虛擬機器就允許自旋的時間更長,否則,如果自旋很少成功,就會取消自旋操作。

鎖消除

一些程式碼做了同步處理,但是執行緒執行中不存在競爭共享資料的可能,就會消除鎖,這是虛擬機器自發的一種優化手段。

鎖粗化

一般在做同步處理的時候,都會盡可能的將同步區域限制小,如果一段程式碼多次獲取同一個鎖,虛擬機器會對同步區域進行擴大是的這段程式碼只需要獲取一次鎖,這種擴大同步區域的優化手段,就是鎖粗化。

例如:Stringbuffer連續的append方法,就會將同步區域擴大到第一次append和最後一次append的範圍。

輕量鎖

輕量級鎖不是用來代替重量級鎖的,只是為了在沒有多執行緒競爭的前提下,減少使用重量級鎖互斥產生的效能消耗。

輕量級鎖的加鎖和解鎖都是通過CAS操作完成的,並且如果存在兩個以上的執行緒競爭同一個鎖,輕量級鎖會膨脹為重量級鎖。

什麼輕量級鎖能夠優化鎖的效能?

對於絕大數鎖而言,在同步週期內都是不存在競爭的,那麼使用輕量級鎖的CAS操作就能夠避免互斥同步的開銷。

偏向鎖

輕量級鎖是在無競爭的情況下使用CAS操作消除同步的互斥量,那麼偏向鎖就是無競爭的情況下將整個同步消除。

偏向鎖,會偏向第一個獲取到鎖的執行緒,如果接下來的執行中,沒有執行緒去競爭這個偏向鎖,那麼持有偏向鎖的執行緒就會消除同步。

但是,如果存在另一個執行緒去競爭偏向鎖,偏向鎖將會失效,回覆到未鎖定或者輕量鎖狀態,不會直接升級為重量鎖。

參考:《深入理解Java虛擬機器-JVM高階特性和最佳實踐(第二版)》