什麼是ThreadLocal?mysql
顧名思義它是local variable(線程局部變量)。它的功用很是簡單,就是爲每個使用該變量的線程都提供一個變量值的副本,是每個線程均可以獨立地改變本身的副本,而不會和其它線程的副本衝突。sql
從線程的角度看,就好像每個線程都徹底擁有該變量。數據庫
注意:ThreadLocal不是用來解決共享對象的多線程訪問問題的。安全
1、多線程共享成員變量
在多線程環境下,之因此會有併發問題,就是由於不一樣的線程會同時訪問同一個共享變量,同時進行一系列的操做。多線程
一、例以下面的形式
//這個意思很簡單,建立兩個線程,a線程對全局變量+10,b線程對全局變量-10 public class MultiThreadDemo { public static class Number { private int value = 0; public void increase() throws InterruptedException { //這個變量對於該線程屬於局部變量 value = 10; Thread.sleep(10); System.out.println("increase value: " + value); } public void decrease() throws InterruptedException { //一樣這個變量對於該線程屬於局部變量 value = -10; Thread.sleep(10); System.out.println("decrease value: " + value); } } public static void main(String[] args) throws InterruptedException { final Number number = new Number(); Thread a = new Thread(new Runnable() { @Override public void run() { try { number.increase(); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread b = new Thread(new Runnable() { @Override public void run() { try { number.decrease(); } catch (InterruptedException e) { e.printStackTrace(); } } }); a.start(); b.start(); } }
思考:可能運行的結果:併發

爲了驗證我上面的緣由分析,我修改下代碼:ide
public void decrease() throws InterruptedException { //我在decrease()新添加這個輸出,看下輸出結果 System.out.println("increase value: " + value); value = -10; Thread.sleep(10); System.out.println("decrease value: " + value); }
再看運行結果:(和上面分析的同樣)工具
思考:若是在 private volatile int value = 0;在這裏加上volatile關鍵字結果如何?this

因此總的來講:spa
a線程和b線程會操做同一個 number 中 value,那麼輸出的結果是不可預測的,由於當前線程修改變量以後可是還沒輸出的時候,變量有可能被另一個線程修改.
當如若是要保證輸出我當前線程的值呢?
其實也很簡單:在 increase() 和 decrease() 方法上加上 synchronized 關鍵字進行同步,這種作法實際上是將 value 的 賦值 和 打印 包裝成了一個原子操做,也就是說二者要麼同時進行,要不都不進行,中間不會有額外的操做。
2、多線程不共享全局變量
上面的例子咱們能夠看到a線程操做全局變量,b在去去全局成員變量是a已經修改過的。
若是咱們須要 value 只屬於 increase 線程或者 decrease 線程,而不是被兩個線程共享,那麼也不會出現競爭問題。
一、方式一
很簡單,爲每個線程定義一份只屬於本身的局部變量。
public void increase() throws InterruptedException { //爲每個線程定義一個局部變量,這樣固然就是線程私有的 int value = 10; Thread.sleep(10); System.out.println("increase value: " + value); }
不論 value 值如何改變,都不會影響到其餘線程,由於在每次調用 increase 方法時,都會建立一個 value 變量,該變量只對當前調用 increase 方法的線程可見。
二、方式二
藉助於上面這種思想,咱們能夠建立一個map,將當前線程的 id 做爲 key,副本變量做爲 value 值,下面是一個實現
public class SimpleImpl { //這個至關於工具類 public static class CustomThreadLocal { //建立一個Map private Map<Long, Integer> cacheMap = new HashMap<>(); private int defaultValue ; public CustomThreadLocal(int value) { defaultValue = value; } //進行封裝一層,其實就是經過key獲得value public Integer get() { long id = Thread.currentThread().getId(); if (cacheMap.containsKey(id)) { return cacheMap.get(id); } return defaultValue; } //一樣存放key,value public void set(int value) { long id = Thread.currentThread().getId(); cacheMap.put(id, value); } } //這個類引用工具類,固然也能夠在這裏寫map。 public static class Number { private CustomThreadLocal value = new CustomThreadLocal(0); public void increase() { value.set(10); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("increase value: " + value.get()); } public void decrease() { value.set(-10); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("decrease value: " + value.get()); } } public static void main(String[] args) throws InterruptedException { final Number number = new Number(); Thread a = new Thread(new Runnable() { @Override public void run() { number.increase(); } }); Thread b = new Thread(new Runnable() { @Override public void run() { number.decrease(); } }); a.start(); b.start(); } }
思考,運行結果如何?
//運行結果(其中一種): increase value: 0 decrease value: -10
按照常理來說應該是一個10,一個-10,怎麼都想不通會出現0,也沒有想明白是哪一個地方引發的這個線程不一樣步,畢竟我這裏兩個線程各放各的key和value值,並且key也不同
爲何出現有一個不存在key值,而取出默認值0。
其實緣由就在HashMap是線程不安全的,併發的時候設置值,可能致使衝突,另外一個沒設置進去。若是這個改爲Hashtable,就發現永遠輸出10和-10兩個值。
3、ThreadLocal
其實上面的方式二實現的功能和ThreadLocal像,只不過ThreadLocal確定更完美。
一、瞭解ThreadLocal類提供的幾個方法
public T get() { } public void set(T value) { } public void remove() { } protected T initialValue() { }
get()方法:獲取ThreadLocal在當前線程中保存的變量副本。
set()方法:用來設置當前線程中變量的副本。
remove()方法:用來移除當前線程中變量的副本。
initialValue()方法:是一個protected方法,通常是用來在使用時進行重寫的,它是一個延遲加載方法,下面會詳細說明。
這裏主要看get和set方法源碼
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
經過這個能夠總結出:
(1)get和set底層仍是一個ThreadLocalMap實現存取值
(2)咱們在放的時候只放入value值,那麼它的key其實就是ThreadLocal類的實例對象(也就是當前線程對象)
二、小案例
public class Test { //建立兩個ThreadLocal對象 ThreadLocal<Long> longLocal = new ThreadLocal<Long>(); ThreadLocal<String> stringLocal = new ThreadLocal<String>(); public static void main(String[] args) throws InterruptedException { final Test test = new Test(); ExecutorService executors= Executors.newFixedThreadPool(2); executors.execute(new Runnable() { @Override public void run() { test.longLocal.set(Thread.currentThread().getId()); test.stringLocal.set(Thread.currentThread().getName()); System.out.println(test.longLocal.get()); System.out.println(test.stringLocal.get()); } }); executors.execute(new Runnable() { @Override public void run() { test.longLocal.set(Thread.currentThread().getId()); test.stringLocal.set(Thread.currentThread().getName()); System.out.println(test.longLocal.get()); System.out.println(test.stringLocal.get()); } }); } }
思考,運行結果如何?

4、ThreadLocal的應用場景
最多見的ThreadLocal使用場景爲 用來解決 數據庫鏈接、Session管理等。
一、 數據庫鏈接管理
同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。
public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "username", "password"); } catch (SQLException e) { e.printStackTrace(); } return conn; } }; public static Connection getConnection() { return connectionHolder.get(); } public static void setConnection(Connection conn) { connectionHolder.set(conn); } }
這樣就保證了一個線程對應一個數據庫鏈接,保證了事務。由於事務是依賴一個鏈接來控制的,如commit,rollback,都是數據庫鏈接的方法。
二、Session管理
private static final ThreadLocal threadSession = new ThreadLocal(); public static Session getSession() throws InfrastructureException { Session s = (Session) threadSession.get(); try { if (s == null) { s = getSessionFactory().openSession(); threadSession.set(s); } } catch (HibernateException ex) { throw new InfrastructureException(ex); } return s; }