執行緒安全與鎖優化——執行緒安全
文章目錄
什麼是執行緒安全?許多對執行緒安全的定義都不恰當,這是Brian Goetz的描述:
當多個執行緒訪問一個物件時,如果不用考慮這些執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那這個物件是執行緒安全的。
一、java中的執行緒安全
java中,按執行緒安全程度由強都弱:不可變、絕對執行緒安全、相對執行緒安全、執行緒相容、執行緒對立。
1.1 不可變
不可變物件一定是執行緒安全的。不可變的物件有String,基本型別的包裝類,Number類的物件。一直說他們是不可變的,那麼他們為什麼是不可變的呢?
首先,什麼是不可變?
對於基本型別的變數,當用final修飾時就行了。然而對於物件來說,保證該物件的行為(方法)不會改變物件的狀態(例項成員),而保證物件不改變物件的狀態的最簡單的方式就是將物件的狀態用final修飾就行(個人看法,只要你不修改狀態就行了,用不用final修飾無所謂)。
現在我們來看看String是怎樣實現的?
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];//用final修飾了
/** Cache the hash code for the string */
private int hash; // Default to 0,這個沒有final修飾,但是它只能在建構函式中被修改(初始化)
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;//用final修飾了
所以看到下面這段程式碼不要感到疑惑(為什麼s是不可變物件,可它卻被重新賦值了)。
String s=new String("123");
s=new String("321");//是被重新賦值
還有
class SuperClass{
private int a;
public void setA(int a){
this.a=a;
}
}
class SubClass extends SuperClass{
private int b;
public void setB(int b){
this.b=b;
}
}
public static void main(String args[]){
//這裡只能說SubClass產生的物件不是不可變物件,因為有setB改變物件狀態的值。你只能說sc這個基本型別(java虛擬機器層面的基本型別reference)是不可變變數。
final SubClass sc=new SubClass();
sc.setB(3);
}
1.2 絕對執行緒安全
java中的絕對執行緒安全的定義就是Brian Goetz所描述的。於是我們在java中常說的絕對執行緒安全的物件就不是絕對執行緒安全的了。
反例:Vector、Hashtable、List。比如Vector
import java.util.Vector;
public class Test {
public static void main(String argc[]) {
Vector<Integer> v= new Vector<>();
for(int i=0;i<10;++i) {
v.add(i);
}
Thread tremove=new Thread(()-> {
for(int i=0;i<v.size();++i) {//這裡使用到了v,v預設使用了final修飾
System.out.println("v刪除了"+v.remove(i));
}
});
Thread tget=new Thread(()->{
for(int i=0;i<v.size();++i) {
System.out.println("v查詢得到"+v.get(i));
}
});
tremove.start();
tget.start();
while(Thread.activeCount()>0);
}
}
上面這個是執行緒安全的。
import java.util.Vector;
public class Test {
public static Vector<Integer> v = new Vector<>();
public static void main(String argc[]) {
for (int j=0;j<2000;++j) {
for (int i = 0; i < 10; ++i) {
v.add(i);
}
Thread tremove = new Thread(() -> {
for (int i = 0; i < v.size(); ++i) {// 這裡使用到了v,v預設使用了final修飾,A
System.out.println("v刪除了" + v.remove(i));//B
}
});
Thread tget = new Thread(() -> {
for (int i = 0; i < v.size(); ++i) {//C
System.out.println("v查詢得到" + v.get(i));//D
}
});
tremove.start();
tget.start();
//while(Thread.activeCount()>20);
}
// while(Thread.activeCount()>0);
}
}
這個時候就報錯了
Exception in thread "Thread-499" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 8
at java.util.Vector.get(Vector.java:751)
at Test.lambda$1(Test.java:19)
v刪除了4
at java.lang.Thread.run(Thread.java:748)
這是為什麼呢?
當兩個執行緒執行到A,C時,獲取的size()值都是正確的,因為size方法是個同步方法。然而到了B,D時,假如tremove執行緒先執行,將對應位置的元素刪了,然後v重新調整空間大小,然後tget繼續執行,他就可能在相應位置獲取不到元素,因為實際的v的大小已經改變了。
為了避免出現問題,我們分別對兩個for迴圈進行同步控制
Thread tremove = new Thread(() -> {
synchronized (v) {// 這裡
for (int i = 0; i < v.size(); ++i) {// 這裡使用到了v,v預設使用了final修飾
System.out.println("v刪除了" + v.remove(i));
}
}
});
Thread tget = new Thread(() -> {
synchronized (v) {// 這裡
for (int i = 0; i < v.size(); ++i) {
System.out.println("v查詢得到" + v.get(i));
}
}
});
1.3 相對執行緒安全
上面舉得那個例子就是相對執行緒安全的。相對執行緒安全是指物件的單獨的操作是執行緒安全的。比如上面例子中對v的size()、remove()、get()的單獨操作是安全的,可以使對for迴圈及裡面的remove()/get()操作就不是執行緒安全的。
Vector、Hashtable、List這些都是相對執行緒安全的。
1.4 執行緒相容
需要進行同步控制就能實現執行緒安全。這種執行緒安全是指物件的單獨操作都不是執行緒安全的,可通過同步控制後就變成了執行緒安全的了。
1.5 執行緒對立
就算做了同步控制,也不能包裝執行緒安全。比如Thread類的suspend()(使執行緒掛起)和resume()(恢復 因suspend()方法掛起的執行緒,使之重新能夠獲得CPU執行).
二、執行緒安全的實現方法
2.1 互斥同步
互斥同步最主要的實現方式是使用sychnorized關鍵字。其原理:
每一個同步程式碼塊的開始和結束都分別有monitorenter與monitorexit兩條位元組碼指令(同步直譯器可以是任何物件,但推薦是臨界資源)。當執行monitorenter指令時,同步監視器的鎖+1,當執行monitorexit時,同步監視器的鎖-1。對於同一個執行緒,synchronized程式碼塊是可重入的。對於簡單的同步塊,狀態切換消耗的時間可能比程式碼真正執行的時間還要長。
reentryantLock也是實現同步的一種方式。可以說reentryantLock是API層面的同步實現方式,而synchronized是原生語法層面的同步實現方式。synchronized與reentryantLock的區別如下:
- 在進行執行緒通訊時,reentryantLock可以用newCondition來產生多個condition物件。然而synchronized只能使用wait(),notifyAll()(相當於一個隱式的condition)。即reentryantLock對應1個或多個condition,而synchronized對應與一個condition。
- 等待可中斷。什麼是等待可中斷,當一個執行緒等待排它鎖久了時,可以放棄等待轉而處理其他事情。這是renentryantLock所有的,而synchronized不具有的。
- 公平鎖。當有很多執行緒等待排他鎖時,必須按照他們申請鎖的時間給與排他鎖。而在synchronized中,獲取鎖的執行緒是隨機的。其實reentryantLock預設情況下也是非公平鎖的,但是可以在構造時傳入引數指定該reentryantLock是公平鎖的。
在jdk1.5之前,renentryantLock的效能的確優與synchronized。然而,在jdk1.6及其之後,對synchronized進行了優化,在效能方法已經不輸於renentryantLock了,所以在jdk1.6之後如果能夠用synchronized實現執行緒安全的情況下,推薦使用synchronized。
2.2 非阻塞的同步方式
什麼叫做非阻塞同步方式。
用synchronized方式、reentryantLock方式實現的同步是阻塞的同步方式。因為每一次都要將沒有獲取到排他鎖的執行緒切換為阻塞狀態,這是要消耗相對多的時間的。這是一種悲觀鎖,如果要實現執行緒安全就要進行狀態切換等。而非阻塞同步方式,它是一種基於樂觀併發策略的樂觀鎖實現的。它會先執行,如果不存在共享資源競爭,那自然就成功了;如果存在共享資源競爭,那它會採取補救措施(最簡單的補救措施就是不斷重試)。
非阻塞同步方式的實現是需要物理指令的支援的。樂觀併發策略需要的物理指令:
- 測試並設定(Test-and-Set)
- 獲取並增加(Fetch-and-Increment)
- 交換(Swap)
- 比較並交換(Compare-and-Swap,CAS)
- 載入連結/條件儲存(Load-Linked/Store-Conditional,LL/SC)
在java中,通過Unsafe類來提供CAS操作。例子,解決volatile在併發情況下不具有:
import java.util.concurrent.atomic.AtomicInteger;
public class Test {
// public static Integer race=0;
public static AtomicInteger race=new AtomicInteger(0);//這裡
private static final int THREAD_COUNT=20;//同時執行的執行緒數
public static void increase(){
//race++;
race.incrementAndGet();//這裡
}
public static void main(String[] args) {
Thread[] threads=new Thread[THREAD_COUNT];
for(int i=0;i<THREAD_COUNT;i++){
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
for(int j=0;j<10000;j++){
increase();
}
}
});
threads[i].start();
}
//讓所有執行緒都結束
while (Thread.activeCount()>1){
Thread.yield();//讓主執行緒讓步一下,是開啟的所有子執行緒都執行完
}
System.out.println(race);
}
}
這樣結果就一定為:200000。
CAS的缺點,雖然CAS看起來很完美,但是它不能使用與所有場景。在java中,Unsafe不是給使用者程式使用的,只有bootstrap載入的類才可以房屋Unsafe.getUnSafe()。所以在不通過反射載入當前類的情況下,只能使用java API來間接使用Unsafe類。所以下面會報錯。
try {
Class.forName("sun.misc.Unsafe");
Unsafe.getUnsafe();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
錯誤:
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:68)
at Test.main(Test.java:16)
2.3 無同步方案
執行緒安全並不一定都需要進行同步。同步只是保證共享資料爭用時的正確性手段。只要一個方法本身不涉及共享資料,那麼他本身就是執行緒安全的。入下面兩種情況
可重入程式碼
在程式碼執行的任意時刻中斷它,然後執行其他程式碼。在回到原來的位置,繼續執行,執行後的結果不出錯誤(和預期一致),那麼這就是可重入程式碼。可重入程式碼具有如下特徵:不依賴堆上的資料和公用的系統資源,用到的狀態量都由引數中傳入、不呼叫非可重入的方法等。
執行緒本地儲存
如果存在共享資料,那麼看共享資料是否可以將其可見範圍限制在同一個執行緒之內,這樣無需同步也能保證執行緒安全。Java中可以通過TheadLocal來實現執行緒本地儲存。