ThreadLocal

什麼是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

  volatile結果

因此總的來講: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;
}
複製代碼
相關文章
相關標籤/搜索