通過《java并發(fā):進程與線程》已經大致了解了促使進程線程產生的原因,以及java中線程的操作方式,那么多線程并發(fā)操作下會產生什么問題呢?
A.線程安全
當多個線程訪問一個類時,如果不用考慮這些線程在運行時環(huán)境下的調度和交替執(zhí)行,并且不需要額外的同步,這個類的行為仍然是正確的,那么稱這個類是線程安全的。
B.引起線程安全問題的因素
無狀態(tài)就是線程安全
多線程編程或者分布式編程最忌諱有狀態(tài),一有狀態(tài)就不但限制了其橫向擴展能力,也是產生并發(fā)問題的起源。當你設計的類是無狀態(tài)的,那么它永遠都是線程安全的。因此在設計階段需要考慮如何用無狀態(tài)的類來滿足你的業(yè)務需求
原子操作
所謂原子性,是說一個操作不會被其他線程打斷,能保證其從開始到結束獨享資源連續(xù)執(zhí)行完這一操作。如果所有程序塊都是原子性的,那么就不存在任何并發(fā)問題。而很多看上去像是原子性的操作正式并發(fā)問題高災區(qū)。比如所熟知的計數(shù)器(count++)和check-then-act,這些都是很容易被忽視的,例如大家所常用的惰性初始化模式,以下代碼就不是線程安全的:
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
Public synchronized ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
這段代碼具體問題在于沒有認識到if(instance==null)和instance = new ExpensiveObject();(假設沒有synchronized)是兩條語句,放在一起就不是原子性的,就有可能當一個線程執(zhí)行完if(instance==null)后會被中斷,另一個線程也去執(zhí)行if(instance==null),這次兩個線程都會執(zhí)行后面的instance = new ExpensiveObject();這也是這個程序所不希望發(fā)生的。
雖然check-then-act從表面上看很簡單,但卻普遍存在與我們日常的開發(fā)中,特別是在數(shù)據庫存取這一塊。比如我們需要在數(shù)據庫里存一個客戶的統(tǒng)計值,當統(tǒng)計值不存在時初始化,當存在時就去更新。如果不把這組邏輯設計為原子性的就很有可能產生出兩條這個客戶的統(tǒng)計值。
另外還有:
非原子的64位操作,JVM允許將64位讀或寫劃分為兩個32位的操作。
可見性
我們不僅要避免一個線程修改其他線程正在使用的對象的狀態(tài),還希望確保黨一個線程修改了對象的狀態(tài)之后,其他的線程能夠真正看到改變。這就是:內存可見性。
看下面的例子:
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
上面的程序可能打印出0(重排序,在number可見之前,ready就已經寫入,并對讀取線程可見),或者永遠不會終止,這是因為它沒有保證寫入ready和number的值對讀線程是可見的。
一些確保線程安全的方法
訪問共享的,可變的數(shù)據,要求同步,為了保證變量元素的可見性,可以采用如下方法:
① 線程封閉
最簡單的方式就是不共享數(shù)據,如果數(shù)據僅在單線程中訪問,就不需要任何同步。線程封閉技術是實現(xiàn)線程安全的最簡單的方式,當對象封閉在一個線程中,這種做法會自動成為線程安全的,即使被封閉的對象本身并不是。比如JDBC的連接池,雖然JDBC本身規(guī)范并沒有要求Connection對象是線程安全的,但是在典型的服務器應用中,線程總是從池中獲得一個Connection對象,并且用它處理一個單一的請求,最后把它歸還,每個線程都會同步地處理大多數(shù)請求,而且在Connection對象在被歸還前,池不會將它再分配給其他線程
② 棧限制
將變量限制在方法中。
③ ThreadLocal
它允許將變量和線程關聯(lián)在一起,使得每個線程都有一份單獨的拷貝。
④ 不可變性
不可變對象永遠是線程安全的,一個對象是不可變的餓,要求它的狀態(tài)創(chuàng)建后不會改變,所有域都是final類型,并且,它被正確創(chuàng)建。