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


  大家通常知道,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());
        }
    }

}

  啟動程序:輸出結果

0
1
2
3
4

  看起來一切正常,每個線程好像都有自己關於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
4
4
4
4

  為什么每個線程都輸出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對象中讀取事務上下文,避免了在調用每個方法時都需要傳遞執行上下文信息。

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM