java多執行緒(6)---ThreadLocal
ThreadLocal
什麼是ThreadLocal?
顧名思義它是local variable(執行緒區域性變數)。它的功用非常簡單,就是為每一個使用該變數的執行緒都提供一個變數值的副本,是每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突。
從執行緒的角度看,就好像每一個執行緒都完全擁有該變數。
注意: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(); } }
思考:可能執行的結果:
執行結果
為了驗證我上面的原因分析,我修改下程式碼:
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關鍵字結果如何?
volatile結果
所以總的來說:
a執行緒和b執行緒會操作同一個 number 中 value,那麼輸出的結果是不可預測的,因為當前執行緒修改變數之後但是還沒輸出的時候,變數有可能被另外一個執行緒修改.
當如如果要保證輸出我當前執行緒的值呢?
其實也很簡單:在 increase() 和 decrease() 方法上加上 synchronized 關鍵字進行同步,這種做法其實是將 value 的 賦值 和 列印 包裝成了一個原子操作,也就是說兩者要麼同時進行,要不都不進行,中間不會有額外的操作。
二、多執行緒不共享全域性變數
上面的例子我們可以看到a執行緒操作全域性變數,b在去去全域性成員變數是a已經修改過的。
如果我們需要 value 只屬於 increase 執行緒或者 decrease 執行緒,而不是被兩個執行緒共享,那麼也不會出現競爭問題。
1、方式一
很簡單,為每一個執行緒定義一份只屬於自己的區域性變數。
public void increase() throws InterruptedException { //為每一個執行緒定義一個區域性變數,這樣當然就是執行緒私有的 int value = 10; Thread.sleep(10); System.out.println("increase value: " + value); }
不論 value 值如何改變,都不會影響到其他執行緒,因為在每次呼叫 increase 方法時,都會建立一個 value 變數,該變數只對當前呼叫 increase 方法的執行緒可見。
2、方式二
藉助於上面這種思想,我們可以建立一個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兩個值。
三、ThreadLocal
其實上面的方式二實現的功能和ThreadLocal像,只不過ThreadLocal肯定更完美。
1、瞭解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類的例項物件(也就是當前執行緒物件)
2、小案例
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()); } }); } }
思考,執行結果如何?
執行結果
四、ThreadLocal的應用場景
最常見的ThreadLocal使用場景為 用來解決 資料庫連線、Session管理等。
1、 資料庫連線管理
同一事務多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,都是資料庫連線的方法。
2、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; }
參考
想太多,做太少,中間的落差就是煩惱。想沒有煩惱,要麼別想,要麼多做。少校【12】