用ThreadLocal類實現線程安全的正確姿勢

TillyLattim 8年前發布 | 10K 次閱讀 線程 Java開發

大家通常知道,ThreadLocal類可以幫助我們實現線程的安全性,這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了get與set等訪問接口或方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。從概念上看,我們把ThreadLocal<T>理解成一個包含了Map<Thread,T>的對象,其中Map的key用來標識不同的線程,而Map的value存放了特定該線程的某個值。但是ThreadLocal的實現并非如此,我們以這樣的理解方式去使用ThreadLocal也并不能實現真正的線程安全。

下面我們舉一個例子進行說明,Number是擁有一個int型成員變量的類:

public class Number {

    private int num;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Number [num=" + num + "]";
    }

}

NotSafeThread是一個實現了Runable接口的類,其中我們創建了一個ThreadLocal<Number>類型的變量value,用來存放不同線程的num值,接著我們用線程池的方式啟動了5個線程,我們希望使用ThreadLocal類為5個不同的線程都存放一個Number類型的副本,根除對變量的共享,并且在調用ThreadLocal類的get()方法時,返回與線程關聯的Number對象,而這些Number對象我們希望它們都能跟蹤自己的計數值:

public class NotSafeThread implements Runnable {

    public static Number number = new Number();

    public static int i = 0;

    public void run() {
        //每個線程計數加一
        number.setNum(i++);
                 //將其存儲到ThreadLocal中
        value.set(number);
        //輸出num值
        System.out.println(value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            newCachedThreadPool.execute(new NotSafeThread());
        }
    }

}

啟動程序:輸出結果

看起來一切正常,每個線程好像都有自己關于Number的存儲空間,但是我們簡單的在輸出前加一個延時:

public class NotSafeThread implements Runnable {

    public static Number number = new Number();

    public static int i = 0;

    public void run() {
        //每個線程計數加一
        number.setNum(i++);
        //將其存儲到ThreadLocal中
        value.set(number);
        //延時2秒
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
        }
        //輸出num值
        System.out.println(value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            newCachedThreadPool.execute(new NotSafeThread());
        }
    }

}

運行程序,輸出:

為什么每個線程都輸出4?難道他們沒有獨自保存自己的Number副本嗎?為什么其他線程還是能夠修改這個值?我們看一下ThreadLocal的源碼:

    public void set(Object obj)
    {
        Thread thread = Thread.currentThread();//獲取當前線程
        ThreadLocalMap threadlocalmap = getMap(thread);
        if(threadlocalmap != null)
            threadlocalmap.set(this, obj);
        else
            createMap(thread, obj);
    }

其中getMap方法:

    ThreadLocal.ThreadLocalMap getMap(Thread thread)
    {
        return thread.inheritableThreadLocals;//返回的是thread的成員變量
    }

可以看到,這些特定于線程的值是保存在當前的Thread對象中,并非保存在ThreadLocal對象中。并且我們發現Thread對象中保存的是Object對象的一個引用,這樣的話,當有其他線程對這個引用指向的對象做修改時,當前線程Thread對象中保存的值也會發生變化。這也就是為什么上面的程序為什么會輸出一樣的結果:5個線程中保存的是同一Number對象的引用,在線程睡眠2s的時候,其他線程將num變量進行了修改,因此它們最終輸出的結果是相同的。

那么,ThreadLocal的“為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。”這句話中的“獨立的副本”,也就是我們理解的“線程本地存儲”只能是每個線程所獨有的對象并且不與其他線程進行共享,大概是這樣的情況:

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
        public Number initialValue(){//為每個線程保存的值進行初始化操作
            return new Number();
        }
    };

或者

    public void run() {
        value.set(new Number());
    }

好吧...這個時候估計你會說:那這個ThreadLocal有什么用嘛,每個線程都自己new一個對象使用,只有它自己使用這個對象而不進行共享,那么程序肯定是線程安全的咯。這樣看起來我不使用ThreadLocal,在需要用某個對象的時候,直接new一個給本線程使用不就好咯。

確實,ThreadLocal的使用不是為了能讓多個線程共同使用某一對象,而是我有一個線程A,其中我需要用到某個對象o,這個對象o在這個線程A之內會被多處調用,而我不希望將這個對象o當作參數在多個方法之間傳遞,于是,我將這個對象o放到TheadLocal中,這樣,在這個線程A之內的任何地方,只要線程A之中的方法不修改這個對象o,我都能取到同樣的這個變量o。

再舉一個在實際中應用的例子,例如,我們有一個銀行的BankDAO類和一個個人賬戶的PeopleDAO類,現在需要個人向銀行進行轉賬,在PeopleDAO類中有一個賬戶減少的方法,BankDAO類中有一個賬戶增加的方法,那么這兩個方法在調用的時候必須使用同一個Connection數據庫連接對象,如果他們使用兩個Connection對象,則會開啟兩段事務,可能出現個人賬戶減少而銀行賬戶未增加的現象。使用同一個Connection對象的話,在應用程序中可能會設置為一個全局的數據庫連接對象,從而避免在調用每個方法時都傳遞一個Connection對象。問題是當我們把Connection對象設置為全局變量時,你不能保證是否有其他線程會將這個Connection對象關閉,這樣就會出現線程安全問題。解決辦法就是在進行轉賬操作這個線程中,使用ThreadLocal中獲取Connection對象,這樣,在調用個人賬戶減少和銀行賬戶增加的線程中,就能從ThreadLocal中取到同一個Connection對象,并且這個Connection對象為轉賬操作這個線程獨有,不會被其他線程影響,保證了線程安全性。

代碼如下:

public class ConnectionHolder {

    public static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    };

    public static Connection getConnection(){
        Connection connection = connectionHolder.get();
        if(null == connection){
            connection = DriverManager.getConnection(DB_URL);
            connectionHolder.set(connection);
        }
        return connection;
    }

}

在框架中,我們需要將一個事務上下文(Transaction  Context)與某個執行中的線程關聯起來。通過將事務上下文保存在靜態的ThreaLocal對象中(這個上下文肯定是不與其他線程共享的),可以很容易地實現這個功能:當框架代碼需要判斷當前運行的是哪一個事務時,只需從這個ThreadLocal對象中讀取事務上下文。這種機制很方便,因為它避免了在調用每個方法時都需要傳遞執行上下文信息,然而這也將使用該機制的代碼與框架耦合在一起。

 

來自:http://www.cnblogs.com/qilong853/p/5982878.html

 

 本文由用戶 TillyLattim 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!