天天看點

深入研究java.lang.ThreadLocal類

一,概述

ThreadLocal是什麼呢?其實ThreadLocal并非是一個線程的本地實作版本,它并不是一個Thread,而是threadlocalvariable(線程局部變量)。也許把它命名為ThreadLocalVar更加合适。線程局部變量(ThreadLocal)其實的功用非常簡單,就是為每一個使用該變量的線程都提供一個變量值的副本,是Java中一種較為特殊的線程綁定機制,是每一個線程都可以獨立地改變自己的副本,而不會和其它線程的副本沖突。   從線程的角度看,每個線程都保持一個對其線程局部變量副本的隐式引用,隻要線程是活動的并且 ThreadLocal 執行個體是可通路的;線上程消失之後,其線程局部執行個體的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。   通過ThreadLocal存取的資料,總是與目前線程相關,也就是說,JVM 為每個運作的線程,綁定了私有的本地執行個體存取空間,進而為多線程環境常出現的并發通路問題提供了一種隔離機制。   ThreadLocal是如何做到為每一個線程維護變量的副本的呢?其實實作的思路很簡單,在ThreadLocal類中有一個Map,用于存儲每一個線程的變量的副本。   概括起來說,對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊通路,而後者為每一個線程都提供了一份變量,是以可以同時通路而互不影響。

二,API介紹

ThreadLocal()           建立一個線程本地變量。   T get()           傳回此線程局部變量的目前線程副本中的值,如果這是線程第一次調用該方法,則建立并初始化此副本。   protected  T initialValue()           傳回此線程局部變量的目前線程的初始值。最多在每次通路線程來獲得每個線程局部變量時調用此方法一次,即線程第一次使用 get() 方法通路變量的時候。如果線程先于 get 方法調用 set(T) 方法,則不會線上程中再調用 initialValue 方法。      若該實作隻傳回 null;如果程式員希望将線程局部變量初始化為 null 以外的某個值,則必須為 ThreadLocal 建立子類,并重寫此方法。通常,将使用匿名内部類。initialValue 的典型實作将調用一個适當的構造方法,并傳回新構造的對象。   void remove()           移除此線程局部變量的值。這可能有助于減少線程局部變量的存儲需求。如果再次通路此線程局部變量,那麼在預設情況下它将擁有其 initialValue。   void set(T value)           将此線程局部變量的目前線程副本中的值設定為指定值。許多應用程式不需要這項功能,它們隻依賴于 initialValue() 方法來設定線程局部變量的值。   在程式中一般都重寫initialValue方法,以給定一個特定的初始值。

三,典型執行個體

1、Hiberante的Session 工具類HibernateUtil 這個類是Hibernate官方文檔中HibernateUtil類,用于session管理。

public class HibernateUtil {
    private static Log log = LogFactory.getLog(HibernateUtil.class);
    private static final SessionFactory sessionFactory;     //定義SessionFactory
 
    static {
        try {
            // 通過預設配置檔案hibernate.cfg.xml建立SessionFactory
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            log.error("初始化SessionFactory失敗!", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    //建立線程局部變量session,用來儲存Hibernate的Session
    public static final ThreadLocal session = new ThreadLocal();
 
    /**
     * 擷取目前線程中的Session
     * @return Session
     * @throws HibernateException
     */
    public static Session currentSession() throws HibernateException {
        Session s = (Session) session.get();
        // 如果Session還沒有打開,則新開一個Session
        if (s == null) {
            s = sessionFactory.openSession();
            session.set(s);         //将新開的Session儲存到線程局部變量中
        }
        return s;
    }
 
    public static void closeSession() throws HibernateException {
        //擷取線程局部變量,并強制轉換為Session類型
        Session s = (Session) session.get();
        session.set(null);
        if (s != null)
            s.close();
    }
}
           

在這個類中,由于沒有重寫ThreadLocal的initialValue()方法,則首次建立線程局部變量session其初始值為null,第一次調用currentSession()的時候,線程局部變量的get()方法也為null。是以,對session做了判斷,如果為null,則新開一個Session,并儲存到線程局部變量session中,這一步非常的關鍵,這也是“public static final ThreadLocal session = new ThreadLocal()”所建立對象session能強制轉換為Hibernate Session對象的原因。   2、另外一個執行個體 建立一個Bean,通過不同的線程對象設定Bean屬性,保證各個線程Bean對象的獨立性。

/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:45:02
 * 學生
 */
public class Student {
    private int age = 0;   //年齡
 
    public int getAge() {
        return this.age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}
           
/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:53:33
 * 多線程下測試程式
 */
public class ThreadLocalDemo implements Runnable {
    //建立線程局部變量studentLocal,在後面你會發現用來儲存Student對象
    private final static ThreadLocal studentLocal = new ThreadLocal();
 
    public static void main(String[] agrs) {
        ThreadLocalDemo td = new ThreadLocalDemo();
        Thread t1 = new Thread(td, "a");
        Thread t2 = new Thread(td, "b");
        t1.start();
        t2.start();
    }
 
    public void run() {
        accessStudent();
    }
 
    /**
     * 示例業務方法,用來測試
     */
    public void accessStudent() {
        //擷取目前線程的名字
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running!");
        //産生一個随機數并列印
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println("thread " + currentThreadName + " set age to:" + age);
        //擷取一個Student對象,并将随機數年齡插入到對象屬性中
        Student student = getStudent();
        student.setAge(age);
        System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
    }
 
    protected Student getStudent() {
        //擷取本地線程變量并強制轉換為Student類型
        Student student = (Student) studentLocal.get();
        //線程首次執行此方法的時候,studentLocal.get()肯定為null
        if (student == null) {
            //建立一個Student對象,并儲存到本地線程變量studentLocal中
            student = new Student();
            studentLocal.set(student);
        }
        return student;
    }
}
           

運作結果:

a is running!
thread a set age to:76
b is running!
thread b set age to:27
thread a first read age is:76
thread b first read age is:27
thread a second read age is:76
thread b second read age is:27 
           

可以看到a、b兩個線程age在不同時刻列印的值是完全相同的。這個程式通過妙用ThreadLocal,既實作多線程并發,遊兼顧資料的安全性。

四,總結

ThreadLocal使用場合主要解決多線程中資料資料因并發産生不一緻問題。ThreadLocal為每個線程的中并發通路的資料提供一個副本,通過通路副本來運作業務,這樣的結果是耗費了記憶體,單大大減少了線程同步所帶來性能消耗,也減少了線程并發控制的複雜度。   ThreadLocal不能使用原子類型,隻能使用Object類型。ThreadLocal的使用比synchronized要簡單得多。   ThreadLocal和Synchonized都用于解決多線程并發通路。但是ThreadLocal與synchronized有本質的差別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該隻能被一個線程通路。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間通路到的并不是同一個對象,這樣就隔離了多個線程對資料的資料共享。而Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得資料共享。   Synchronized用于線程間的資料共享,而ThreadLocal則用于線程間的資料隔離。   當然ThreadLocal并不能替代synchronized,它們處理不同的問題域。Synchronized用于實作同步機制,比ThreadLocal更加複雜。

五、ThreadLocal使用的一般步驟

1、在多線程的類(如ThreadDemo類)中,建立一個ThreadLocal對象threadXxx,用來儲存線程間需要隔離處理的對象xxx。 2、在ThreadDemo類中,建立一個擷取要隔離通路的資料的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離通路類型的對象,并強制轉換為要應用的類型。 3、在ThreadDemo類的run()方法中,通過getXxx()方法擷取要操作的資料,這樣可以保證每個線程對應一個資料對象,在任何時刻都操作的是這個對象。