JAVA-線程安全性

線程安全性:
一個類是線程安全的是指在被多個線程訪問時,類能夠持續進行正確的行爲.不用考慮這些線程運行時環境下的調度和交替.
 
編寫正確的併發程序的關鍵在於對共享的,可變的狀態進行訪問管理.
解決方案有兩種:
1.控制資源訪問.經過鎖機制來對資源的訪問進行排隊.這樣來避免一個線程修改其餘線程正在使用的對象
2.要確保當一個線程修改了對象的狀態後,其餘的線程可以真正知道這種變化.
 
資源訪問控制
-------------------------------
1. 無狀態的類確定是線程安全的,由於它不會存在交替的狀況.由於全部要用到的資源都是經過參數傳進去的.這樣就不會存在多個線程共享資源的問題.
 
2.若是是 有狀態的類,好比它有個屬性是long count;它有個方法,是讓它自增:count++; http://blog.sina.com.cn/s/blog_5f54f0be0100vwh8.html此文中已經介紹了該操做併發的風險.在代碼中該操做看起來是一個單獨的操做,但它其實是由三個操做組成的.因此它不是 單獨的,不可分割的.即:" 原子性".原子性不能指程序上的最基本的數字邏輯操做,而是邏輯上的不可分割的操做.
 
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變量只能保證可見性.因此,在不須要原子性的時候,能夠用它.

相關文章
相關標籤/搜索