1. 程式人生 > >java多執行緒(6)---ThreadLocal

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】