Java多線程——線程封閉

線程封閉:當訪問共享的可變數據時,一般須要同步。一種避免同步的方式就是不共享數據。若是僅在單線程內訪問數據,就不須要同步,這種技術稱爲線程封閉(thread  confinement)java

  線程封閉技術一個常見的應用就是JDBC的Connection對象,JDBC規範並無要求Connection對象必須是線程安全的,在服務器應用程序中,線程從鏈接池獲取一個Connection對象,使用完以後將對象返還給鏈接池。下面介紹幾種線程封閉技術:mysql

  一、Ad-hoc線程封閉sql

  Ad-hoc線程封閉是指,維護線程的封閉性的職責徹底由程序實現承擔,是很是脆弱的,所以在程序中儘可能少使用,通常使用更強的線程封閉技術,好比棧封閉或者ThreadLocal類。安全

二、棧封閉  服務器

  棧封閉是線程封閉的一種特列,在棧封閉中,只能經過局部變量才能訪問對象。局部變量的固有屬性之一就是封閉在執行棧中,其餘線程沒法訪問這個棧,棧封閉也稱爲線程內部使用或者線程局部使用。簡單的說就是局部變量。多個線程訪問一個方法,此方法中的局部變量都會被拷貝一分兒到線程棧中。因此局部變量是不被多個線程所共享的,也就不會出現併發問題。因此能用局部變量就別用全局的變量,全局變量容易引發併發問題。多線程

  好比下面的例子:併發

public class Snippet {
    public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;
    
        // animals被封閉在方法中,不要使它們逸出!
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            if (candidate == null || !candidate.isPotentialMate(a))
                candidate = a;
            else {
                ark.load(new AnimalPair(candidate, a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }
}

  在loadTheArk中實例化一個TreeSet對象,並將該對象的一個引用保存到animals中。此時,只有一個引用指向集合animals,這個引用被封閉到局部變量中,所以也被封閉到局部變量中。然而,若是發佈了對集合animals(或者該對象中的任何內部數據)的引用,那麼封閉性將被破壞,並致使對象animals的逸出。ide

三、ThreadLocal類this

  維持線程封閉性的一種更加規範方法是使用ThreadLocal類,這個類能使線程中某個值與保存值的對象關聯起來。ThreadLocal類提供了get和set等訪問接口或者方法,這些方法爲每一個使用該變量的線程都存在一份獨立的副本,所以get老是放回當前執行線程在調用set設置的最新值。看一下下面代碼例子:spa

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

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

  經過調用ConnectionManager.getConnection()方法,每一個線程獲取到的,都是本身獨立擁有的一個的Connection對象副本,第一次獲取時,是經過initialValue()方法的返回值來設置值的。經過ConnectionManager.setConnection(Connection conn)方法設置的Connection對象,也只會和當前線程綁定。這樣就實現了Connection對象在多個線程中的徹底隔離。在Spring容器中管理多線程環境下的Connection對象時,採用的思路和以上代碼很是類似。

  每一個線程是怎麼和Connection對象副本綁定的?這個對象副本保存在哪裏。當某個線程初次調用ThreadLocal類的get方法時,就會調用initialValue來獲取初始值,從概念上看,咱們能夠將ThreadLocal<T>視爲包含了Map<thread, T>對象,其中保存了特定於該線程的值,可是ThreadLocal的實現並不是如此,這樣只是爲了咱們方便理解而已。

  下面咱們來分析一下ThreadLocal的源碼。ThreadLocal類的方法很簡單,只有四個,分別爲set,get,remove, initialValue,從字面上咱們也能理解這些方法的做用。

  public T get():返回當前線程所對應的局部變量。

  public void set(T arg0):設置當前線程局部變量的值。  

  public void remove():將當前線程局部變量的值刪除,目的是爲了減小內存的佔用,該方法是JDK 5.0新增的方法。注意,當線程結束後,對應該線程的局部變量將自動被垃圾回收,因此顯式調用該方法清除線程的局部變量並非必須的操做,但它能夠加快內存回收的速度。

  protected T initialValue(): 對當線程局部變量進行初始化,並返回該初始值。是protected 屬性,顯然是讓子類進行對其覆蓋重寫的,只有第一次調用set和get方法時才調用。  

  下面咱們對這四個方法的源碼進行分析,看看ThreadLocal類是如何實現這種「爲每一個線程提供不一樣的變量拷貝」。

3.1 set方法

  如下是set方法的源碼

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to 
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

  從set方法中能夠看到,首先獲取當前線程:Thread arg1 = Thread.currentThread();

  再獲取當前線程的ThreadLocalMap:ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);

  判斷ThreadLocalMap是否爲空,不爲空,則以鍵值對的形式設置值,key爲this,value就是局部變量的副本,this是當前線程持有的ThreadLocal類實例化對象。

  假如爲空,則經過createMap方法建立。

  咱們看下getMap和createMap方法的源碼:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

  從代碼上已經寫的很是清楚,每一個線程都有本身的局部變量的副本,該副本是存在ThreadLocalMap 中,其中鍵值就是ThreadLocal類實例化對象。也就是說每一個線程都擁有本身的ThreadLocalMap,ThreadLocalMap保存的就是局部變量副本。咱們看一下java.lang.Thread源碼。

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

3.2 get方法

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

  從代碼上看,前兩步和set方法是一個樣的,分別獲取當前線程和當前線程的ThreadLocalMap,第三步判斷ThreadLocalMap是否爲空,不爲空根據this鍵值獲取value,爲空調用setInitialValue()方法。

  如下是setInitialValue方法代碼:

private T setInitialValue() {
        Object arg0 = this.initialValue();
        Thread arg1 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
        if (arg2 != null) {
            arg2.set(this, arg0);
        } else {
            this.createMap(arg1, arg0);
        }
        return arg0;
}

  在setInitialValue裏調用了initialValue()方法,也就是子類要重寫覆蓋的方法,對應上面的例子的代碼是:

protected Connection initialValue() {  
            Connection conn = null;  
            try {  
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
            return conn;  
}

  而後獲取當前線程和當前線程的ThreadLocalMap,ThreadLocalMap爲空則調用createMap,不然調用set方法。

3.3 總結

  ThreadLocalMap對象是以this指向的ThreadLocal對象爲鍵進行查找的,這固然和前面set()方法的代碼是相呼應的。

  進一步地,咱們能夠建立不一樣的ThreadLocal實例來實現多個變量在不一樣線程間的訪問隔離,爲何能夠這麼作?由於不一樣的ThreadLocal對象做爲不一樣鍵,固然也能夠在線程的ThreadLocalMap對象中設置不一樣的值了。經過ThreadLocal對象,在多線程中共享一個值和多個值的區別,就像你在一個HashMap對象中存儲一個鍵值對和多個鍵值對同樣,僅此而已。

  也就說,每一個線程都有一個ThreadLocalMap,該線程訪問到某個局部變量,且該局部變量是用ThreadLocal類進行聲明時,該線程就會new ThreadLocal(),而後將該ThreadLocal類的對象做爲key值,所對應的局部變量做爲value值保存到ThreadLocalMap中。當線程訪問多個ThreadLocal類進行聲明局部變量時,在ThreadLocalMap中就有多個鍵值對。而每一個線程都有本身的ThreadLocalMap,從而達到隔離的目的了。

  當某個線程終止後,該線程裏的ThreadLocalMap也被回收了,因此徹底不用擔憂內存泄漏的問題。

  假如多線程訪問的對象實例是單例的,或者說只能建立一個,那就老老實實的使用同步機制(synchronized)了.

相關文章
相關標籤/搜索