Java 并发基础——线程安全性
線程安全:多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,并且在主調代碼中不需要任何額外的同步或協調,這個類都能表現出正確的行為,那么久稱這個類是線程安全的。
在線程安全類中封裝了必要的同步機制,因此客戶端無需采取進一步的同步措施。
原子性
要么不執行,要么執行到底。原子性就是當某一個線程修改i的值的時候,從取出i到將新的i的值寫給i之間不能有其他線程對i進行任何操作。也就是說保證某個線程對i的操作是原子性的,這樣就可以避免數據臟讀。 通過鎖機制或者CAS(Compare And Set 需要硬件CPU的支持)操作可以保證操作的原子性。
當多個線程訪問某個狀態變量,并且其中有一個線程執行寫入操作時,必須采用同步機制來協調這些線程對變量的訪問。無狀態對象一定是線程安全的。
? 如果我們在無狀態的對象中增加一個狀態時,會出現什么情況呢?假設我們按照以下方式在servlet中增加一個"命中計數器"來管理請求數量:在servlet中增加一個long類型的域,每處理一個請求就在這個值上加1。
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() {
return count ;
}
@Override
public void service(ServletRequest arg0, ServletResponse arg1)
throws ServletException, IOException {
// do something
count++;
}
}
不幸的是,以上代碼不是線程安全的,因為count++并非是原子操作,實際上,它包含了三個獨立的操作:讀取count的值,將值加1,然后將計算結果寫入count。如果線程A讀到count為10,馬上線程B讀到count也為10,線程A加1寫入后為11,線程B由于已經讀過count值為10,執行加1寫入后依然為11,這樣就丟失了一次計數。
??????? 在 count++例子中線程不安全是因為 count++并非原子操作,我們可以使用原子類,確保確保操作是原子,這樣這個類就是線程安全的了。
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() {
return count .get() ;
}
@Override
public void service(ServletRequest arg0, ServletResponse arg1)
throws ServletException, IOException {
// do something
count.incrementAndGet();
}
}
?
?????? AtomicLong是java.util.concurrent.atomic包中的原子變量類,它能夠實現原子的自增操作,這樣就是線程安全的了。?? 同樣,上述情況還會出現在 單例模式的懶加載過程中,當多個線程同時訪問 getInstance()函數時。這篇文章中有講解:實現優雅的單例模式
加鎖機制
????? 線程在執行被synchronized修飾的代碼塊時,首先檢查是否有其他線程持有該鎖,如果有則阻塞等待,如果沒有則持有該鎖,并在執行完之后釋放該鎖。
????? 除了使用原子變量的方式外,我們也可以通過加鎖的方式實現線程安全性。還是UnsafeCountingFactorizer,我們只要在它的service方法上增加synchronized關鍵字,那么它就是線程安全的了。當然在整個方法中加鎖在這里是效率很低的,因為我們只需要保證count++操作的原子性,所以這里只對count++進行了加鎖,代碼如下:
?
public class UnsafeCountingFactorizer implements Servlet {private long count = 0;public long getCount() {return count ;}@Overridepublic void service(ServletRequest arg0, ServletResponse arg1)throws ServletException, IOException {// do somethingsynchronized(this){count++;}} }?
Synchronized代碼塊使得一段程序的執行具有 原子性,即每個時刻只能有一個線程持有這個代碼塊,多個線程執行在執行時會互不干擾。
java 內存模型及 可見性
? ? ?Java的內存模型沒有上面這么簡單,在Java Memory Model中,Memory分為兩類,main memory和working memory,main memory為所有線程共享,working memory中存放的是線程所需要的變量的拷貝(線程要對main memory中的內容進行操作的話,首先需要拷貝到自己的working memory,一般為了速度,working memory一般是在cpu的cache中的)。被volatile修飾的變量在被操作的時候不會產生working memory的拷貝,而是直接操作main memory,當然volatile雖然解決了變量的可見性問題,但沒有解決變量操作的原子性的問題,這個還需要synchronized或者CAS相關操作配合進行。
每個線程內部都保有共享變量的副本,當一個線程更新了這個共享變量,另一個線程可能看的到,可能看不到,這就是可見性問題。
下面這段代碼中 main 線程中 改變了 ready的值,當開啟多個子線程時,子線程的值并不是馬上就刷新為最新的ready的值(這里的中間刷新的時間間隔到底是多長,或者子線程的刷新機制,自己也不太清楚。當開啟一個線程去執行時,ready值改變時就會立刻刷新,循環立刻就結束,但是當開啟多個線程時,就會有一定的延遲)。
?
public class SelfTest {private static boolean ready;private static int number;private static long time;public static class ReadThread extends Thread {public void run() {while(!ready ){System. out.println("******* "+Thread.currentThread()+""+number);Thread. yield();}System. out.println(number+" currentThread: "+Thread.currentThread());}}public static void main(String [] args) {time = System.currentTimeMillis();new ReadThread().start();new ReadThread().start();new ReadThread().start();new ReadThread().start();try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}number = 42;ready = true ;System.out.println("賦值時間:ready = true ");} }?
上面這段代碼的執行結果:可以看出賦值后,循環還是執行了幾次。
此時如果把 ready的屬性加上 volatile 結果便是如下的效果:
由此可見Volatile可以解決內存可見性的問題。
上面講的加鎖機制同樣可以解決內存可見性的問題,加鎖的含義不僅僅局限于互斥行為,還包括內存可見性。為了確保所有線程都能看到共享變量的最新值,所有執行讀操作或者寫操作的線程都必須在同一個鎖上同步。
注:由于System.out.println的執行仍然需要時間,所以這面打印的順序還是可能出現錯亂。
參考:
http://www.mamicode.com/info-detail-245652.html
并發編程實戰
http://www.cnblogs.com/NeilZhang/p/7979629.html
夢想不是浮躁,而是沉淀和積累
總結
以上是生活随笔為你收集整理的Java 并发基础——线程安全性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 投资者注意了,这个20万亿的理财市场,正
- 下一篇: 密度仅提升10% 台积电2nm工艺挤牙膏