1. 程式人生 > >java之ThreadLocal詳解

java之ThreadLocal詳解

.com vat zed 存儲 占用 create 是我 rem obj

一、ThreadLocal簡介

ThreadLocal是線程的局部變量,是每一個線程所單獨持有的,其他線程不能對其進行訪問,通常是類中的private static字段。

我們知道有時候一個對象的變量會被多個線程所訪問,這時就會有線程安全問題,當然我們可以使用synchorinized 關鍵字來為此變量加鎖,進行同步處理,從而限制只能有一個線程來使用此變量,但是加鎖會大大影響程序執行效率,此外我們還可以使用ThreadLocal來解決對某一個變量的訪問沖突問題。

當使用ThreadLocal維護變量的時候 為每一個使用該變量的線程提供一個獨立的變量副本,即每個線程內部都會有一個該變量,這樣同時多個線程訪問該變量並不會彼此相互影響,因此他們使用的都是自己從內存中拷貝過來的變量的副本, 這樣就不存在線程安全問題,也不會影響程序的執行性能。

但是要註意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的占用會比不使用ThreadLocal要大。

二、ThreadLocal源碼分析

(1)ThreadLocal方法

ThreadLocal 的幾個方法: ThreadLocal 可以存儲任何類型的變量對象, get返回的是一個Object對象,但是我們可以通過泛型來制定存儲對象的類型。

public T get() { } // 用來獲取ThreadLocal在當前線程中保存的變量副本
public void set(T value) { } //set()用來設置當前線程中變量的副本
public void remove() { } //remove()用來移除當前線程中變量的副本
protected T initialValue() { } //initialValue()是一個protected方法,一般是用來在使用時進行重寫的

(2)ThreadLocal實現原理

1.內部結構

Thread 在內部是通過ThreadLocalMap來維護ThreadLocal變量表, 在Thread類中有一個threadLocals 變量,是ThreadLocalMap類型的,它就是為每一個線程來存儲自身的ThreadLocal變量的。

ThreadLocal.ThreadLocalMap threadLocals = null;

 ThreadLocalMap 是定義在ThreadLocal 類裏的內部類,它的作用是存儲線程的局部變量。ThreadLocalMap 以ThreadLocal的引用作為鍵,以局部變量作為值,存儲在ThreadLocalMap.Entry (一種存儲鍵值的數據結構)裏。這是因為在每一個線程裏面,可能存在著多個ThreadLocal變量

static class ThreadLocalMap {
 static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

2.調用過程

初始時,在Thread裏面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。
然後在當前線程裏面,如果要使用副本變量,就可以通過get方法在threadLocals裏面查找

註意:

在進行get之前,必須先set,否則會報空指針異常;

如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。
  

ThreadLocal的set(T value)方法

public void set(T value) {
  // 獲得當前線程
Thread t = Thread.currentThread();
  // 獲得當前線程的 ThreadLocalMap 引用,詳細見下
ThreadLocalMap map = getMap(t);
  // 如果不為空,則更新局部變量的值
if (map != null)
  map.set(this, value);
  //如果不是第一次使用,先進行初始化
else
  createMap(t, value);
}

內部類ThreadLocalMap的set(ThreadLocal<?> key,Object value)

    private void set(ThreadLocal<?> key, Object value) {
 Entry[] tab = table;
 int len = tab.length;
    // Hash 尋址,與table數組長度減1(二進制全是1)相與,所以數組長度必須為2的次方,減小hash重復的可能性
 int i = key.threadLocalHashCode & (len-1);

   //從hash值計算出的下標開始遍歷
 for (Entry e = tab[i];
      e != null;
      e = tab[i = nextIndex(i, len)]) {
   //獲得該Entry的鍵
   ThreadLocal<?> k = e.get();
    //如果鍵和傳過來的相同,覆蓋原值,也說明,一個ThreadLocal變量只能為一個線程保存一個局部變量
   if (k == key) {
     e.value = value;
     return;
   }
   // 鍵為空,則替換該節點
   if (k == null) {
     replaceStaleEntry(key, value, i);
     return;
   }
 }

 tab[i] = new Entry(key, value);
 int sz = ++size;
   //是否需要擴容
 if (!cleanSomeSlots(i, sz) && sz >= threshold)
   rehash();
}

可以看出ThreadLocalMap 采用線性探測再散列解決Hash沖突的問題。即,如果一次Hash計算出來的數組下標被占用,即hash值重復了,則在該下標的基礎上加1測試下一個下標,直到找到空值。比如說,Hash計算出來下標i為6,table[6] 已經有值了,那麽就嘗試table[7]是否被占用,依次類推,直到找到空值。以上,就是保存線程本地變量的方法。

TheadLocal的get()方法

public T get() {
  //獲得當前線程    
Thread t = Thread.currentThread();
  //得到當前線程的一個threadLocals 變量
ThreadLocalMap map = getMap(t);
if (map != null) {
  // 如果不為空,以當前ThreadLocal為主鍵獲得對應的Entry
  ThreadLocalMap.Entry e = map.getEntry(this);
  if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value;
    return result;
  }
}
  //如果值為空,則進行初始化
return setInitialValue();
}

ThreadLocal的setInitialValue()方法

private T setInitialValue() {
  //獲得初始默認值
T value = initialValue();
  //得到當前線程
Thread t = Thread.currentThread();
  // 獲得該線程的ThreadLocalMap引用
ThreadLocalMap map = getMap(t);
  //不為空則覆蓋
if (map != null)
    map.set(this, value);
else
      //若是為空,則進行初始化,鍵為本ThreadLocal變量,值為默認值
    createMap(t, value);
}

// 默認初始化返回null值,這也是 下面demo 為什麽需要重寫該方法的原因。如果沒有重寫,第一次get()操作獲得的線程本地變量為null,需要進行判斷並手動調用set()進行初始化
protected T initialValue() {
    return null;
}

三、Demo

下面是個ThreadLocal使用的實例,兩個任務共享同一個變量,並且兩個任務都把該變量設置為了線程私有變量,這樣,雖然兩個任務都”持有“同一變量,但各自持有該變量的拷貝。因此,當一個線程修改該變量時,不會影響另一線程該變量的值。

package thread.ThreadLocalTest;

import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* Created by StoneGeek on 2018/8/1.
* 博客地址:http://www.cnblogs.com/sxkgeek
*/
public class ThreadLocalDemo2 implements Runnable {
    // 一般會把 ThreadLocal 設置為static 。它只是個為線程設置局部變量的入口,多個線程只需要一個入口
    private static ThreadLocal<Student> localStudent = new ThreadLocal() {
    // 一般會重寫初始化方法,一會分析源碼時候會解釋為什麽
        @Override
        public Student initialValue() {
            return new Student();
    }
};

private Student student = null;

@Override
public void run() {
    String threadName = Thread.currentThread().getName();

    System.out.println("【" + threadName + "】:is running !");

    Random ramdom = new Random();
    //隨機生成一個變量
    int age = ramdom.nextInt(100);

    System.out.println("【" + threadName + "】:set age to :" + age);
    // 獲得線程局部變量,改變屬性值
    Student stu = getStudent();
    stu.setAge(age);

    System.out.println("【" + threadName + "】:第一次讀到的age值為 :" + stu.getAge());

    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("【" + threadName + "】:第二次讀到的age值為 :" + stu.getAge());
}

public Student getStudent() {
    student = localStudent.get();

//         如果不重寫初始化方法,則需要判斷是否為空,然後手動為ThreadLocal賦值,否則的話會報空指針異常
//        if(student == null){
//            student = new Student();
//            localStudent.set(student);
//        }

    return student;
}

public static void main(String[] args) {
    ThreadLocalDemo2 ll = new ThreadLocalDemo2();
    Thread t1 = new Thread(ll, "線程1");
    Thread t2 = new Thread(ll, "線程2");

    t1.start();
    t2.start();
}
}

console打印:
【線程2】:is running !
【線程1】:is running !
【線程1】:set age to :67
【線程2】:set age to :4
【線程1】:第一次讀到的age值為 :67
【線程2】:第一次讀到的age值為 :4
【線程1】:第二次讀到的age值為 :67
【線程2】:第二次讀到的age值為 :4

四、應用場景

最常見的ThreadLocal使用場景為
用來解決 數據庫連接、Session管理等。

數據庫連接

Class A implements Runnable{
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
       return connectionHolder.get();
}
}

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;
}

java之ThreadLocal詳解