線程安全(Java語言)

在剛學習Java線程安全這節內容的時候,一直以爲"線程安全"這個命題是一個非真即假的二元排他選項,即要麼是安全的,要麼是不安全的;


其實不然,Brian Goetz在IBM developWorkers上發表的一篇論文中,他把各種操作共享的數據分成5類:

1、不可變的

在講述不可變之前,先來了解一個知識:在JDK5.0之後,Java內存模型被修正之後,不可變(Immutable)的對象一定是線程安全的;

因此,無論是對象的方法實現,又或者是方法的調用者,都不需要再採取任何的線程安全保障措施。

好,現在回到我們"不可變"的這話題上來,說到不可變,很多童鞋第一反應可能就是關鍵字final,也確實是final;

不過要注意的一點:當一個不可變的對象被正確地創建出來(沒有發生this引用逃逸的情況),那其外部的可見狀態永遠也不會改變。


可能看文字大夥覺得不是那麼舒服,那我們來看一下下面的這段代碼:

private final int value = 1;
當value被構建出來後,其的值永遠都是1,這個是不會改變的。


但是有的人會說,以下的代碼可以通過編譯,可以運行:

final StringBuffer buffer = new StringBuffer("immutable");
buffer.append("csdn");
第一個例子的情況是value爲基本數據類型,而第二個例子buffer則是一個引用變量;

我們這裏講的是,value和buffer變量所對應的那塊內存空間的內容不可改變,

並不是buffer所指的另一塊內存空間的值。簡單來說,就是指"引用變量"不能變,引用變量所指的對象中的內容是可以改變的。


像我們經常接觸到的java.lang.String類則是一個典型的不可變的對象,我們通過調用它的substring()、replace()和concat()這些方法都不會影響原來的值,

這些方法所返回的是一個新構造的字符串對象。


另外,不可變的類型,除了String,還有枚舉類型,以及java.lang.Number的部分子類,如Long和Double等數值包裝類型,BigInteger和BigDecimal等大數據類型。


2、絕對線程安全的

如果想達到絕對線程安全這個級別,需要付出的代價往往是很大的,有的時候甚至是不切實際的代價。

在Java API中聲明自己是線程安全的類,大多數都不是絕對線程安全的。

例如java.util.Vector類,它的add()、get()、remove()和size()等方法都是被synchronized修飾的,不要以爲使用這些方法的時候,就不需要額外的同步手段了;

如果是這樣想的話,那代價一定是很大的;不信我們來看看下面這段代碼:

package com.gdut.test;

import java.util.Vector;

public class ThreadSecurity {

	private static Vector<Integer> vector = new Vector<Integer>();
	
	public static void main(String[] args) {
		
		while(true) {
			// 往vector裏面添加元素
			for(int i = 0; i < 10; i++) {
				vector.add(i);
			}
			
			// 移除vector內元素的線程
			Thread removeThread = new Thread(new Runnable() {
				@Override
				public void run() {
					for(int i = 0; i < vector.size(); i++) {
						// 移除元素
						vector.remove(i);
					}
				}
			});
			

			// 打印vector內元素的線程
			Thread printThread = new Thread(new Runnable() {
				@Override
				public void run() {
					for(int i = 0; i < vector.size(); i++) {
						// 打印輸出元素
						System.out.println(vector.get(i));
					}
				}
			});
			
			// 分別啓動兩個線程
			removeThread.start();
			printThread.start();
			
			// 限制線程數
			while(Thread.activeCount() > 20) ;
		}
	}

}

輸出的結果所拋的異常:

Exception in thread "Thread-9755404" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 4

導致這個異常的原因就是:

當removeThread線程恰好在錯誤的時候刪除了一個元素,導致序號i已經不再可用,再用i去訪問數組就會拋出一個 java.lang.ArrayIndexOutOfBoundsException


所以,我們只要修改一下代碼即可:

// 移除vector內元素的線程
Thread removeThread = new Thread(new Runnable() {
	@Override
	public void run() {
		synchronized(vector) {
			for(int i = 0; i < vector.size(); i++) {
				// 移除元素
				vector.remove(i);
			}
		}
		
	}
});


// 打印vector內元素的線程
Thread printThread = new Thread(new Runnable() {
	@Override
	public void run() {
		synchronized(vector) {
			for(int i = 0; i < vector.size(); i++) {
				// 打印輸出元素
				System.out.println(vector.get(i));
			}
		}
	}
});

這樣就保證了vector訪問的線程安全性。

這裏的"絕對"的意思就是:一個類,不管運行時環境如何,調用者都不需要任何額外的同步措施


3、相對線程安全的

相對線程安全就是我們通常意義上所講的線程安全,即上面vector例子中,它的remove、add等方法和HashTable等都是屬於這一類型的。


4、線程兼容的

線程兼容是指對象本身並不是線程安全的,例如ArrayList和HashMap等類,但是可以通過在調用端正確地使用同步手段來保證對象在併發環境中可以安全地使用。

我們平常說一個類不是線程安全的,絕大多數時候指的是這一種情況。


5、線程對立的

線程對立是指無論調用端是否採取了同步措施,都無法在多線程環境中併發使用的代碼;而這種線程對立的代碼,往往是有害的,應該儘量去避免。

例如Thread.suspend()和Thread.resume()方法,如果suspend()中斷的線程就是即將要執行resume的那個線程,這樣就產生死鎖了。





內容參考於:周志明  深入理解java虛擬機

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章