所謂可見性,指的是當一個線程修改了對象的狀態後,其餘線程可以看到該對象發生的變化。在單線程環境下,向某個變量寫入值,而後在後面的操做再讀取,在這個過程當中該變量的值對該線程來講老是可見。可是,在多線程環境下,可見性就不必定等到保證,例如,對於一個共享變量 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
「指令重排序」指的是處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。對於上面的代碼,ready 和 number 誰先賦值對最終的程序結果並無影響(對於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.html。jvm
不過這裏有一個點要注意,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 類爲每一個線程保存了一份獨立的副本,每次線程執行 ThreadLocal 的 get 或 set 方法都是每次以當前線程爲參數去取當前線程對象裏的 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類線程封閉的具體原理了。
以上內容若有不當之處,請指出。謝謝!
參考連接: