1. 程式人生 > >執行緒安全與鎖優化——執行緒安全

執行緒安全與鎖優化——執行緒安全

文章目錄


什麼是執行緒安全?許多對執行緒安全的定義都不恰當,這是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的區別如下:

  1. 在進行執行緒通訊時,reentryantLock可以用newCondition來產生多個condition物件。然而synchronized只能使用wait(),notifyAll()(相當於一個隱式的condition)。即reentryantLock對應1個或多個condition,而synchronized對應與一個condition。
  2. 等待可中斷。什麼是等待可中斷,當一個執行緒等待排它鎖久了時,可以放棄等待轉而處理其他事情。這是renentryantLock所有的,而synchronized不具有的。
  3. 公平鎖。當有很多執行緒等待排他鎖時,必須按照他們申請鎖的時間給與排他鎖。而在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來實現執行緒本地儲存。