java併發編程可見性與線程封閉

可見性

  所謂可見性,指的是當一個線程修改了對象的狀態後,其餘線程可以看到該對象發生的變化。在單線程環境下,向某個變量寫入值,而後在後面的操做再讀取,在這個過程當中該變量的值對該線程來講老是可見。可是,在多線程環境下,可見性就不必定等到保證,例如,對於一個共享變量 share = 0 來講,線程1和線程2都進行share++ 操做,可是最終share 的結果並不必定是2。先看看一段代碼html

public class NoVisibility {

   private static boolean ready;

   private static int number;

   private static class ReaderThread extends Thread{
      public void run() {
         while (!ready) {
            Thread.yield(); //當前線程從運行態->就緒態,從新競爭cpu
         }
         System.out.println(number);
      }
   }

   public static void main(String[] args) {
      new ReaderThread().start();
      number = 42;
      ready = true;
   }
}

  上面的 NoVisibility 可能會一直循環下去(雖然這種狀況發生的機率很小),由於 ReaderThread 線程一直看不到主線程對ready的更新;還有另外一種狀況是輸出結果多是0,有人會問有輸出說明 ready 已經被更新爲 true,那麼 number 也應該被更新了42,看起來是這樣,但在jvm執行指令時會出現「指令重排序」的現象。java

  「指令重排序」指的是處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。對於上面的代碼,readynumber 誰先賦值對最終的程序結果並無影響(對於main線程來講),故在實際執行可能先對 ready 賦值。可是要注意的是若是後面的語句對前面的語句存在依賴關係時,則不會發生「指令重排序」,例如數據庫

int a = 2;    //語句1
int r = 2;    //語句2
a = a + 3;    //語句3
r = a*a;     //語句4

語句4 的 r 依賴於語句3的 a 的處理結果,故執行時語句4不能在語句3以前執行安全

  在上面 NoVisibility 產生錯誤的緣由是缺少同步使得 ReadTread 線程讀取了失效數據。另外有一點,對於絕大多數基本變量的讀取和寫入都是原子操做,可是64位的數值變量除外(如long,double),這是由於對於64位的變量,jvm容許將64位的讀或寫操做分解爲兩個32位操做,當對該變量讀和寫操做在不一樣線程進行,有可能會讀取到當前值的高32位和從新賦值後的低32位,如線程1去讀 long num 這個變量,還未開始讀取,這時線程2對num這個變量從新賦值,先對低32位進行更新,還未更新高32位,這時線程1繼續執行讀取操做,因而線程1就讀取了原來的高32位和更新後的低32位。多線程

  那麼怎麼使得不一樣線程不會讀取到失效數據呢?一種簡單的方式是加上內置鎖,這樣使得某一線程在還沒有執行完同步代碼塊前,其餘的線程沒法執行同步代碼塊,這樣就保證了每一個線程都能看到共享變量的最新值;另外的一種方式是把共享變量用 volatile 修飾,線程在讀取volatile 變量時老是會返回最新寫入的值,關於volatile更多詳細請看下面參考連接http://www.javashuo.com/article/p-hsugcpqq-hw.htmljvm

       不過這裏有一個點要注意,volatile 雖然保證了變量的可見性,但並不保證原子性,這也是爲何用 volatile 修飾的 int i 變量執行 i++ 操做仍不能保證線程安全,其實這要從i++ 這個操做的原理來說,i++包括三個操做:一、取 i 值;二、將 i+1 存入 tmp;三、i = tmp。在多線程對 valotile 修飾的 i 進行++操做時,先假設線程1執行完第2步後堵塞這時線程2對 i 進行了更新,通知其餘線程內存中的 i 已經被更新,大家應該從新取 i 值,但線程1在第3步並不須要取 i 值,而是將 tmp 值存入i。因此在使用volatile關鍵字是應該要記住它並不保證原子性。優化

線程封閉

  什麼是線程封閉?當咱們訪問共享變量時,一般要使用同步,一種避免使用同步的方式就是不共享數據。很顯然僅在單線程內訪問數據,就不須要同步,這種避免共享數據的技術就是線程封閉。在java中,較經常使用到的線程封閉技術是棧封閉和使用 ThreadLocal 類。this

  棧封閉:個人理解就是使用線程內部的局部變量。spa

  ThreadLocal 類:這個要仔細講講,ThreadLocal 類爲每一個線程保存了一份獨立的副本,每次線程執行 ThreadLocalgetset 方法都是每次以當前線程爲參數去取當前線程對象裏的 ThreadLocalMap,而 ThreadLocalMap 保存着以 ThreadLocal 對象爲 key 的鍵值對,這樣就使得每一個線程訪問 ThreadLocal 變量互不干擾。先來看看ThreadLocal類怎麼使用。線程

public class Test {
   private static ThreadLocal<Connection> conn
      = new ThreadLocal<Connection>() {
      // 重寫 ThreadLocal類裏的initialValue()方法
      public Connection initialValue() {
         try {
            return DriverManager.getConnection("DB_URL");//取得某一數據庫鏈接
         } catch (SQLException e) {
            e.printStackTrace();
         }
         return null;
      }
   };

   public static Connection getConnection() {
      // 調用conn對象的get方法
      return conn.get();
   }
}

下面看看ThreadLocal類的源碼

先看看 get 方法

public ThreadLocal() {

}
public T get() {
        Thread t = Thread.currentThread();

/*
* 查看當前線程t有沒有相應的map,注意,該方法傳入的參數爲當前線程,
* 返回的是線程t的靜態變量 threadLocals,該變量初始值爲null,故對不一樣
* 線程來講,每一個線程都有本身的threadLocals
*/ ThreadLocalMap map = getMap(t); /*
*     ThreadLocalMap getMap(Thread t) { *      return t.threadLocals; *     } */ if (map != null) { // 獲取當前線程下對象的value,注意,每一個線程都存有當前對象的value ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 若是線程是第一次調用ThreadLocal的get方法,那麼返回值從重寫的初始化方法獲得 return setInitialValue(); }

 

去看看 setInitialValue() 方法

private T setInitialValue() {
        // 初始化value
        T value = initialValue();
        Thread t = Thread.currentThread();
        // 取當前線程的map
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 不爲空,更新當前線程下該對象的value
            map.set(this, value);
        else
         // map爲空,建立map
            createMap(t, value);
        return value;
}

 

看看 ThreadLocal 怎麼建立 ThreadLocalMap

// 這個方法是使得ThreadLocal類保存線程本地變量的關鍵,它新建的ThreadLocalMap是以當前ThreadLocal對象爲key,而後是存在該線程的靜態變量threadLocals裏。

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 對於table裏的引用,每次都是new出來了,故不會和其餘線程指向同一個當前對象
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
 }

看到這裏,相信你們就已經大概明白ThreadLocal類線程封閉的具體原理了。

以上內容若有不當之處,請指出。謝謝!

參考連接:

http://www.javashuo.com/article/p-oiitcxzh-gv.html

http://www.javashuo.com/article/p-hsugcpqq-hw.html

相關文章
相關標籤/搜索