線程安全
“當(dāng)多個線程訪問某個類時,不管運(yùn)行時環(huán)境采用何種調(diào)度方式,或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行為,那么就稱這個類是線程安全的”。
對象的狀態(tài)是指存儲在狀態(tài)變量(實(shí)例或者靜態(tài)域)中的數(shù)據(jù)。
“共享”意味著變量可以有多個線程同時訪問,而“可變”意味著變量的值在其生命周期內(nèi)可以變化。
編寫線程安全的代碼的核心在于:要對狀態(tài)訪問操作進(jìn)行管理,特別時對共享的(shared)和可變(Mutable)的狀態(tài)的訪問。
一個對象是否需要是線程安全的,取決于它是否需要被多個線程訪問,不需要被多線程訪問,則不談它的安全性。要使對象是線程安全的,需要采用同步機(jī)制來協(xié)同對象可變狀態(tài)的訪問,如果無法實(shí)現(xiàn)協(xié)同,則可能導(dǎo)致數(shù)據(jù)被破壞以及錯誤結(jié)果。
無狀態(tài)對象一定時線程安全的,如Servlet,就沒有定義任何的屬性。所以多個線程調(diào)用它的方法總能得到正確的結(jié)果。但是自定義的Servlet子類中如果定義了一些狀態(tài),那么共享對象需要同步機(jī)制來保證對象可變狀態(tài)的訪問。
內(nèi)存可見性
- 做到內(nèi)存可見性的原則:對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行stroe、write操作)
必看-內(nèi)存可見性視頻 - 多個線程對共享變量進(jìn)行讀寫操作時,如果線程A修改了共享變量,其它線程應(yīng)該能夠知道這個修改。實(shí)現(xiàn)方法有:synchronized、final、Volatile變量。
- 枷鎖來實(shí)現(xiàn)內(nèi)存可見,內(nèi)置鎖可以用于確保某個線程以一種可預(yù)測的方式來查看另一個線程的執(zhí)行結(jié)果。
深入理解Java虛擬機(jī)筆記---原子性、可見性、有序性
重排序
- JVM為了能夠充分利用多核處理器的強(qiáng)大性能,在缺乏同步的情況下,Java內(nèi)存模型允許編譯器對操作順序進(jìn)行重排序,并將數(shù)值寄存在寄存器中。此外,他還允許CPU對操作順序進(jìn)行重排序,并將計(jì)算值緩存在處理器特定的緩存中。(如果沒有重排序存在,在編寫并發(fā)代碼時可以省去一些事,但是這是存在的所以需要做一些事情來防止對關(guān)鍵代碼的重排序)
- 以下代碼中主線程中的代碼可能存在重排序,即在缺少同步情況下,JVM允許編譯器和CPU對操作順序進(jìn)行重排序,那么最后 number = 42; ready = true;語句的執(zhí)行順序就會顛倒變?yōu)? ready = true;number = 42; 。這對ReaderThread線程來說是個悲劇,因?yàn)樗赡芟茸x到ready=true,在執(zhí)行還沒等主線程為number設(shè)置42,就執(zhí)行了輸出number操作,結(jié)果就為0。那么這種結(jié)果不是我期望的42結(jié)果。這就是由于多個線程之間對內(nèi)存寫入操作的不可見導(dǎo)致的結(jié)果。實(shí)現(xiàn)內(nè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;
}
}
- java提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性
正確性
某個類的行為與其規(guī)范完全一致。在良好的規(guī)范中通常會定義各種不變性條件(Invariant)來約束對象的狀態(tài)s,以及定義各種后驗(yàn)條件(Postcondition)來描述對象操作的結(jié)果。
- 補(bǔ)充:不變性條件可能涉及對象的多個狀態(tài),比如,對象的a狀態(tài)變化時b也要變化,如果這個不變性在多線程中會被破壞了則該類不是線程安全的,因此當(dāng)不變性臺條件涉及多個變量時,當(dāng)更新某一個變量時,需要在同一個原子操作中對其它變量同時進(jìn)行更新。可用鎖實(shí)現(xiàn)。
非原子性的64位操作
內(nèi)存可見性和原子性:Synchronized和Volatile的比較
Java內(nèi)存模型要求,變量的讀取操作和寫入操作必須時原子操作,但對于非volatile類型的long和double變量,JVM允許將64位的讀操作和寫操作分解為兩個32位的操作,當(dāng)讀取一個非volatile類型的long變量時,如果對該變量的讀操作和寫操作在不同的線程中執(zhí)行,那么很可能會讀取到某個值的高32位和另一個值的低32位,因此在多線程中使用共享且可變(某個線程會對該變量執(zhí)行寫操作)的long和double等類型的bain了也是不安全的,除非使用volatile來聲明他們或者使用鎖保護(hù)起來。(也許以后的處理器就都可以提供64位數(shù)值的原子操作)
volatile變量
java語言提供一種稍弱的同步機(jī)制,即volatile變量,用來確保將變量的更新操作通知到其它線程。當(dāng)把變量聲明位volatile類型后,編譯器運(yùn)行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內(nèi)存操作一起重排。voldatilte變量不會被緩存到寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型變量時總會返回最新寫入的值。
什么叫將變量的更新操作通知到其它線程?首先該變量是一個共享變量,可以被多個線程訪問,就是上面講的內(nèi)存可見性,
不要過度使用volatile變量,僅當(dāng)volatile變量能簡化代碼的實(shí)現(xiàn)以及對同步策略的驗(yàn)證時,才應(yīng)該使用他們,如果在驗(yàn)證正確性時(某個類的行為與其規(guī)范完全一致)需要對可見性進(jìn)行復(fù)雜的判斷,那么就不要使用volatile變量。
volatile變量的一種典型用法:檢查某個狀態(tài)標(biāo)記以判斷是否退出循環(huán)。下面示例中,線程通過數(shù)綿羊的方法進(jìn)入休眠。為了使這個示例能正確執(zhí)行,asleep必須為volatile變量。否則當(dāng)asleep被另一個線程修改時,執(zhí)行判斷的線程卻發(fā)現(xiàn)不了。為什么發(fā)現(xiàn)不了?答:JVM在server模式中(另一個模式client做了相對較少的優(yōu)化)對代碼進(jìn)行了更多的優(yōu)化,其中就包括將循環(huán)中未被修改的變量提升到循環(huán)外部,對于該代碼中,asleep在while中沒有被修改,如果asleep不是volatile類型,那么JVM就會將asleep的判斷條件提升到循環(huán)體外部,這將導(dǎo)致一個無線循環(huán)。
public class CountingSheep {
volatile boolean asleep;
void tryToSleep() {
while (!asleep)
countSomeSheep();
}
void countSomeSheep() {
// One, two, three...
}
}
volatile變量只能確保可見性,不能確保原子性,而加鎖機(jī)制兩種都可以。因?yàn)関olatile的語義不足以確保遞增操作(count++)的原子性,除非你能確保只有一個線程對變量執(zhí)行寫操作。在訪問volatile變量時不會執(zhí)行加鎖操作,因?yàn)橐簿筒粫?zhí)行線程阻塞。
-
當(dāng)且僅當(dāng)滿足以下條件時才使用volatile變量:
- 對變量的寫入操作不依賴變量的當(dāng)前值,或者你能確保只有單個線程更新變量的值。
- 該變量不會與其它狀態(tài)變量一起納入不變性條件中。(在良好的規(guī)范中通常會定義各種不變性條件Invariant來約束對象的狀態(tài)s,以及定義各種后驗(yàn)條件Postcondition來描述對象操作的結(jié)果,)
- 在訪問變量時不需要枷鎖。
-
補(bǔ)充不變性條件:不變性條件可能涉及對象的多個狀態(tài),比如,對象的a狀態(tài)變化時b也要變化,如果這個不變性在多線程中會被破壞了則該類不是線程安全的,因此當(dāng)不變性臺條件涉及多個變量時,當(dāng)更新某一個變量時,需要在同一個原子操作中對其它變量同時進(jìn)行更新。可用鎖實(shí)現(xiàn)。
- 在LinkedList集合中存在多個不變性條件,其中一條如下:鏈表的第一個節(jié)點(diǎn)指針first和最后一個節(jié)點(diǎn)指針last的不變性關(guān)系。
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
發(fā)布與逸出
- 發(fā)布一個對象:是對象能能在當(dāng)前作用域之外的代碼中使用。當(dāng)發(fā)布一個對象可能會間接地發(fā)布其他對象。當(dāng)發(fā)布一個對象時,在該對象的非私有域中引用的所有對象同樣會被發(fā)布。
- 逸出:當(dāng)某個不應(yīng)該被發(fā)布的對象被發(fā)布時,這種情況稱為逸出。
- 發(fā)布對象方法:
- 將對象的引用保持到一個公有靜態(tài)變量中。
- 指向該對象的應(yīng)用保持到其它代碼可以訪問的地方
- 在一個非私有方法中返回對象的引用。
- 發(fā)布一個內(nèi)部的類實(shí)例。如下代碼:ThisEscape 發(fā)布EventListener時,也隱含的發(fā)布了ThisEscape實(shí)例本身,因?yàn)檫@個內(nèi)部類實(shí)例中保護(hù)了對EventListener實(shí)例的隱含引用。這種非期望的發(fā)布就造成了ThisEscape對象的逸出。
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
- 防止逸出:
不要在構(gòu)造過程中使用this引用。
如果想著構(gòu)造函數(shù)中注冊一個事件監(jiān)聽器或者啟動線程,那么可以使用一個私有的構(gòu)造函數(shù)和一個公共的工廠方法。如下:
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
競態(tài)條件與復(fù)合操作
- 當(dāng)某個計(jì)算的正確性取決于多個線程的交替執(zhí)行時序時,那么就會發(fā)生競態(tài)條件。出現(xiàn)競態(tài)條件,就可能會造成線程不安全。
下面代碼就顯示了延遲初始化中的競態(tài)條件,它破壞了這個類的正確性。
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
以下代碼存在競態(tài)條件,count++包含了”讀取-修改-寫入”三個操作。
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
....
}
- 常見競態(tài)條件:先檢查后執(zhí)行、讀取-修改-寫入。
- 避免競態(tài)條件產(chǎn)生的線程不安全問題,這些操作應(yīng)該是原子性的。即為了確保線程安全性,把”先檢查后執(zhí)行“、”讀取-修改-寫入“等操作統(tǒng)稱為復(fù)合操作:包含了一組必須以原子方式執(zhí)行的操作。
- 實(shí)現(xiàn)復(fù)合操作的原子性:枷鎖機(jī)制(可實(shí)現(xiàn)多個狀態(tài)的原子操作)、原子變量類(針對只有一個狀態(tài))
- 為了實(shí)現(xiàn)這種復(fù)合操作的原子性可以使用加鎖機(jī)制。對于一些只包含一個狀態(tài)的復(fù)合操作可以使用java.until.concurrent.atomoc包中包含的一些原子變量類來解決。
- java.until.concurrent.atomoc包中包含的一些原子變量類,用于實(shí)現(xiàn)在數(shù)值和對象引用上的原子狀態(tài)轉(zhuǎn)換。
如下代碼:使用AtomicLong來代替long類型的計(jì)數(shù)器,能夠確保所有對計(jì)數(shù)器狀態(tài)的訪問都是原子的,
public class CountingFactorizer extends GenericServlet implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() { return count.get(); }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
線程封閉
定義:當(dāng)訪問共享的可變數(shù)據(jù)時,通常需要使用同步。一種避免使用同步的方式就是不共享數(shù)據(jù)。如果僅在單線程內(nèi)訪問數(shù)據(jù),就不需要同步,這種技術(shù)被稱為線程封閉技術(shù)(Thread Confinement),它是實(shí)現(xiàn)現(xiàn)在安全性的最簡單方式之一。
案例:JDBC的Connection對象就使用了線程封閉技術(shù),JDBC規(guī)范并不要求Connection對象必須是線程安全的。在典型的服務(wù)器應(yīng)用程序中,線程從連接池中獲得一個Connection對象,并且用該對象來處理請求,使用完后再將對象返還給連接池。由于大多數(shù)請求(如Servlet)都是有單個線程采用同步技術(shù)的方式來處理,并且再Connection對象返回之前,連接池不會再將他分配給其它線程,因此這中連接管理模式再處理請求時隱含的將Connection對象封裝再線程中。
Servlet的多線程和線程安全注:應(yīng)用程序服務(wù)器提供的連接池是線程安全的連接池通常會由多個線程訪問,因此非線程安全的連接池是毫無意義的。
實(shí)現(xiàn)線程封閉性的技術(shù):Java沒有強(qiáng)制規(guī)定某個變量必須有鎖來保護(hù),同樣也無法強(qiáng)制將對象封裝再某個線程中。線程封閉是再程序設(shè)計(jì)中考慮的一個因素。但是Java語言及其核心類庫提供了一些機(jī)制來幫助維持線程封閉性,例如棧封閉和ThreadLocal類,還有一種是Ad-hoc線程封閉,即便如此程序員也需要負(fù)責(zé)確保封閉性在線程中的對象不會從線程中逸出。
Ad-hoc線程封閉(脆弱,不推薦)
維護(hù)線程封閉性的職責(zé)完全有程序?qū)崿F(xiàn)來承擔(dān),很脆弱不建議使用。
如下代碼通過Map實(shí)現(xiàn)線程封閉性:
其中static類型的data是線程間共享的,但是為了實(shí)現(xiàn)數(shù)據(jù)和線程綁定,所以通過map來存放不同線程操作的數(shù)據(jù)data,data的獲取和線程也是綁定的,這樣就實(shí)現(xiàn)了data數(shù)據(jù)在單線程中訪問了,不會與其它線程共享,注map是和其它線程共享的, 這樣每個線程中操作的A、B類都是共享和本線程綁定的那個data,從而不會沖突和出錯。
棧封閉性
- 棧封閉性是線程封閉性的一種特例,再棧封閉中,只能通過局部變量才能訪問對象。
- 局部變量固有屬性之一就是封閉在執(zhí)行線程中。它們位于執(zhí)行線程的棧中,其他線程無法訪問這各棧。棧封閉(也被稱為線程內(nèi)部使用或者線程局部局部使用,不要與核心類庫中的ThreadLocal混淆)比Ad-hoc線程封閉更易維護(hù)。
- 如下:對于loadTheArk方法的局部變量numPairs,無論如何也不會破環(huán)棧的封閉性。因?yàn)槿魏畏椒ǘ紵o法獲得對基本類型的引用,因此java語言的這種語義就確保了基本類型的局部變量始終封閉在線程內(nèi)。
- 在維護(hù)對象引用的棧封閉性時,程序員需要確保被引用的對象不會逸出。loadTheArk方法中animals引用指向了一個SortedSet對象,此時只有一個引用指向了集合animals,這個引用被封閉在了局部變量中,因此也被封閉在執(zhí)行線程中。但是,如果發(fā)布了對集合animals的引用,那么封閉性也被破壞,并導(dǎo)致對象animals逸出。
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
// animals confined to method, don't let them escape!
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
if (candidate == null || !candidate.isPotentialMate(a))
candidate = a;
else {
ark.load(new AnimalPair(candidate, a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
ThreadLocal類(重點(diǎn))
- 這個類能使線程中的某個值與保存該值的對象關(guān)聯(lián)起來。ThreadLocal提供類get與set等訪問接口或方法,這些方法為每個使用該變量的線程都存有一份獨(dú)立的副本,因此get總是返回由當(dāng)前執(zhí)行線程在調(diào)用set時設(shè)置的最新值。
- ThreadLocal對象通常用于防止對可變的單實(shí)例變量(Singleton)或全局變量進(jìn)行共享。如:單線程應(yīng)用中可能會位置一個全局的數(shù)據(jù)庫連接,并在程序啟動時初始化這個連接對象,從而避免在調(diào)用每個方法時都需要傳遞一個Connection對象(實(shí)現(xiàn)線程內(nèi)數(shù)據(jù)共享)。
- 如下代碼,通過將JDBC的連接保存到ThreadLocal對象中,每個線程都會擁有屬于自己的連接。
public class ConnectionDispenser {
static String DB_URL = "jdbc:mysql://localhost/mydatabase";
private ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
try {
return DriverManager.getConnection(DB_URL);
} catch (SQLException e) {
throw new RuntimeException("Unable to acquire Connection, e");
}
};
};
public Connection getConnection() {
return connectionHolder.get();
}
}
更多案例:1.如果需要將一個單線程應(yīng)用移植到多線程環(huán)境中,通過將共享的全局變量轉(zhuǎn)換為ThreadLocal(如果全局變量的語義允許),可以維持線程安全性。2.在EJB調(diào)用期間,J2EE容器需要將一個事務(wù)上下文(Transaction Context)與某個執(zhí)行中的線程關(guān)聯(lián)起來。通過將Transaction Context保存在靜態(tài)的ThreadLocal對象中,可以很容易實(shí)現(xiàn)這個功能。
實(shí)現(xiàn)機(jī)制:可以將ThreadLocal<T>視為包含了Map<Thread,T>對象,其中保存了特定于該線程的值,但ThreadLocal的實(shí)現(xiàn)并非如此,這些特定于線程的值保存在Thread對象中,當(dāng)線程終止后,這些值會作為垃圾回收。
注意:不要濫用ThreadLocal,例如將所有全局變量都作為ThreadLocal變量,或者作為“隱藏”方法參數(shù)的手段(設(shè)為全局就不需要通過參數(shù)傳遞過來)。ThreadLocal變量類似全局變量,它降低了代碼的可重用性,并在類之間引入隱含的耦合性(一個線程中或涉及操作多個類,這些類中有的方法就有可能依賴ThreadLocal變量),因此要格外小心。
線程封閉性、線程內(nèi)數(shù)據(jù)共享
一個線程T1內(nèi)操作多個對象A、B時,A、B中操作的數(shù)據(jù)都屬于該線程范圍內(nèi)的。
比如:javaWeb中存錢操作,會操作數(shù)據(jù)庫。
張三開啟T1線程獲取連接connection,然后T1內(nèi)操作取錢類A取錢,操作記錄類B記錄日志,然后進(jìn)行conn提交。
李四開啟T2線程獲取連接connection,然后T1內(nèi)操作取錢類A取錢,操作記錄類B記錄日志,然后進(jìn)行conn提交。
線程間獨(dú)立:以上兩個線程獲取的connection應(yīng)該是獨(dú)立的,只屬于該線程,如果T1和T2共享一個connection,那么如果張三轉(zhuǎn)入錢后還沒來的急轉(zhuǎn)出,就被李四提前轉(zhuǎn)出了,那么就會出錯。 (即實(shí)現(xiàn)線程封閉性)
線程內(nèi)共享:每個線程中的connection對象是對該線程中所有被操作對象都是共享的。
不變性
滿足同步需求的另一種方法是使用不可變對象(Immutable Object).
當(dāng)滿足以下條件,對象才是不可變的:
對象創(chuàng)建以后其狀態(tài)就不能修改。(比如:可通過關(guān)鍵字-》簡單類型狀態(tài)、程序控制實(shí)現(xiàn)-》引用類型狀態(tài))
對象的所有域都是final類型(有例外)。
對象是正確常見的(在創(chuàng)建對象期間,this引用沒有逸出)
不可變性并不等于將對象中所有的域都聲明為final類型,即使都為final類型,這個對象也仍然是可變的,因?yàn)閒inal域可以保存可變對象的引用。
如下代碼:在不可變對象基礎(chǔ)上構(gòu)建不可變類,盡管Set對象是可變的,但從ThreeStooges設(shè)計(jì)中可以看到,在Set對象構(gòu)造完成后,無法對其進(jìn)行修改。(程序控制)
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
public String getStoogeNames() {
List<String> stooges = new Vector<String>();
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
return stooges.toString();
}
}
- 這時候你可能會郁悶,沒有不將域聲明為final也可以啊,為什么要設(shè)為final。答:1.(自己理解)final域能確保初始化過程的安全性。2.其次通過將域聲明為final類型,也相當(dāng)于告訴維護(hù)人員這些域是不會變化的。3.良好的編程習(xí)慣。
Final域
- fianl類型的域是不能修改的,但是如果final域所引用的對象是可變的,那么這些被引用的對象是可以修改的。
- 在JMM中,final域能確保初始化過程的安全性
安全發(fā)布
不正確的發(fā)布:正確的對象被破壞
不可變對象與初始化安全性
安全發(fā)布的常用模式
詳解Java中的clone方法 -- 原型模式
string 在clone()中的特殊性 (轉(zhuǎn)載)
--------待更新