線程安全性:
一個類是線程安全的是指在被多個線程訪問時,類能夠持續進行正確的行爲.不用考慮這些線程運行時環境下的調度和交替.
編寫正確的併發程序的關鍵在於對共享的,可變的狀態進行訪問管理.
解決方案有兩種:
1.控制資源訪問.經過鎖機制來對資源的訪問進行排隊.這樣來避免一個線程修改其餘線程正在使用的對象
2.要確保當一個線程修改了對象的狀態後,其餘的線程可以真正知道這種變化.
資源訪問控制
-------------------------------
1.
無狀態的類確定是線程安全的,由於它不會存在交替的狀況.由於全部要用到的資源都是經過參數傳進去的.這樣就不會存在多個線程共享資源的問題.
JAVA提供了一些線程安全的類,也就是實現了原子性的類.對這些類的操做是原子性的.它們是 在:java.util.concurrent.atomic包中.好比有類:AtomicLong,它是Long的原子化類.咱們對long類型的 count進行自增操做時,不是原子性的,但對AtomicLong調用:incrementAndGet()便是原子操做的,JAVA爲咱們解決了這些 問題.
同時,JAVA提供了咱們本身可控制的原子機制--
鎖.
JAVA提供了強制原子性的內置鎖機制:synchronized .
咱們經過 synchronized 給一個類,或一個方法或一個屬性或一串操做進行鎖標識.線程進入synchronized 以前會自動得到鎖;在正常退出,或出現異常時,線程都會釋放鎖.被鎖上後,其它線程只有等到鎖被釋放才能進入.不然只有一直等下去.因此這種作法在有些時候會極端影響效率.(靜態屬性或方法的鎖是從Class對象上獲取的)
當一個線程請求其餘線程已經佔有的鎖時,請求被阻塞.但佔有鎖的那個線程是能夠再次請求的.這就意味着:鎖的基於線程的而不是基於請求.實現這種機制是爲 每一個鎖關聯一個請求計數和一個佔有它的線程.當計數爲0時,表示該鎖未被佔有.此時線程請求時,JVM將記錄鎖的佔有線程,並將請求計數加1.若是同一線 程再次請求這個鎖,計數再加1.每次退出synchronized 標識的塊時計數會減1.當計數爲0時,鎖被釋放.
並非全部的數據都須要鎖保護--只有那些被多個線程訪問的可變數據才須要.過多的synchronized 會影響性能.因此咱們最好是將一些須要同步的原子操做放在同步塊中.以下面這種作法:
synchronized (this) {html
++hits;java
if (i.equals(lastNumber)) {緩存
++cacheHits;安全
factors = lastFactors.clone();多線程
}併發
}性能
if (factors == null) {this
factors = factor(i);atom
synchronized (this) {spa
lastNumber = i;
lastFactors = factors.clone();
}
}
如上所示.兩個分離的synchronized 塊中都只有很簡短的代碼.第一個塊保護着檢查再運行的操做以檢查對咱們很重要的狀態碼,另外一個進行數據的更新.
共享對象
-------------------------------
同步的可見性:
使用了synchronized進行加鎖後,一個線程在該同步塊內作的操做對接下來的線程是可見的.這就是"同步"的含義.
1.當一個讀線程和一個寫線程同時進行時,咱們不能保證讀線程能及時地讀取寫線程寫入的值.除非使用synchronized進行同步.例以下面代碼所示:
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;
}
上面的mian主線程運行時還充當了"寫線程",而且新建"讀線程"並讓它運行.讀線程會不斷的循環直到ready的值爲true.但在有些狀況下,上面的程序會和咱們想象的輸入42相異:
因爲JAVA的"重排序"機制(JVM:只要代碼順序改變對結果不產生影響,那麼就不能保證代碼執行的順序是書寫的順序)可能在對number設置值前ready的值就已是true了.那麼輸入的結果會是0.
2.在沒有同步時,咱們可能就象上面同樣,得到到的數據不是最新設置進去的.如:一個類有一屬性,而且有它的getter,setter方法,當兩個線程一個執行getter一個執行setter時,就容易出現得到到"
過時數據".但給getter,setter方法加上synchronized 後能夠解決這一問題.
除了過時數據,還可能出現錯數據,這種問題只是存在於64位的數據.因爲JVM的運算是基於32位的.即:不論是布爾值(1位),short(16位),運算時,都經過左側補零將它擴展成32位,而後進行運算.而float,double,long 等64位的數據則被作爲兩個32位數進行運算.
因此,在多線程未同步時,64位數據的讀取可能會返回一個值的前32位,及另外一個值的後32位.經過給值加上
volatile標記可讓JVM避免這種問題.如:volatile float test;
當一個域聲明爲volatile 類型後,編譯器與運行時會監視這個變量,並且對它的操做不會與其餘的內在操做重排序.它不會緩存在寄存器或者緩存在其它地方,因此讀一個volatile 類型的變量時,它老是返回由某一線程所寫入的最新值.咱們能夠將它看作輕量級的同步機制.
private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
如上代碼能夠被:volatile private int value;以及不加同步聲明的getter,setter方法所代替.但固然會犧少量功能:加鎖能夠保證可見性和原子性,但volatile變量只能保證可見性.因此,在不須要原子性的時候,能夠用它.