Java併發編程之基礎篇(三) -- ThreadLocal

Java併發編程之基礎篇(三) – ThreadLocal

ThreadLocal介紹

上篇文章講到,如果想在多線程的環境下,實現共享可變資源的安全訪問,最好的方式是加鎖,也就是同一時刻只有一個線程在使用共享可變資源。如果我們有一種方式可以根除對變量的共享,那麼就可以實現不加鎖的情況下對變量進行安全訪問。

還拿之前搶衛生間坑位的例子舉例,如果只有一個衛生間坑位,五個人都想去衛生間的話,那麼就需要加鎖同步。如果給每個人都提供一個單獨的坑位,那麼就可以不加鎖了,因爲沒有爭搶的場景發生。

Java通過ThreadLocal來實現每個線程都擁有一份自己的共享變量的拷貝。大家可以把ThreadLocal<T>簡單的理解成Map<Thread,T>ThreadLocal提供了getset等方法,get方法總是返回當前線程調用set方法時設置的最新值。如果是第一次調用get方法,將會返回initialValue方法裏面的設置的初始值。

ThreadLocal使用場景

ThreadLocal通常用在防止全局變量的共享,或者單例實例的共享。舉個例子,連接數據庫的時候,首先要創建一個connection連接對象,但是這個connection對象不一定是線程安全的,如果所有線程方法都使用這個對象,進行數據庫的連接,就有可能會出問題。如果使用加鎖進行同步,那麼性能上會有問題,這個時候就可以通過ThreadLocal來幫忙,讓每個線程都持有一份connection對象。這樣就可以完美解決問題。

//設置初始值,通過initialValue()
private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection("DB_URL");
    }
};
    
//通過get()方法獲得ThreadLocal的值
public static Connection getConnection() {
    return connectionThreadLocal.get();
}

各位一定注意 ThreadLocal的使用場景,千萬不要亂用

原子性和可見性
在使用加鎖同步的方式來保證共享資源實現安全訪問的方案中,鎖除了保證資源的原子性以外還對可見性做了保證。

原子性:併發編程裏面的原子性,與數據庫裏面的原子性概念是一致,都是表示操作時不可分割的,必須在不打斷的情況下,一次執行完成。

可見性:在單線程的情況下,一個變量被修改之後,當再次需要使用的時候,肯定會讀取到正確的值,但是在多線程情況下,一個線程修改變量之後,其他線程並不能保證第一時間讀到這個變量。

如果要理解這個問題,需要對JVM的重排序有一定的理解。所謂的重排序就是編譯器會對你寫的代碼進行順序調整,以達到優化運行效率的目的。

對於可見性問題,可以通過如下代碼示例進行說明

public class NoVisibility {
    //private static volatile boolean ready = false;
     private static boolean ready = false;
    private static int number = 0;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready) ;
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(1000);
        number = 34;
        ready = true;
    }
}

這段會啓動一個讀線程,當readytrue時會打印出number的值。然後主線程會修改readynumber的值。如果該段代碼是在client模式下運行,你很可能會看到正確的結果34,但是如果是在server模式下運行,那麼程序可能進入死循環,因爲讀線程看不到主線程對ready的修改。

如果是本地開發環境,JVM一般都是client模式,可以在你的IDE裏面設置JVM的模式爲server模式,運行該段代碼。

如果想讓讀線程及時發現ready變量的修改,可以使用volatile關鍵字對變量ready進行修飾,可以保證所有線程第一時間看到該變量。

對於原子性,Java提供了atomic包,比如對於上篇文章提到的任務計數器示例,我們可以不使用synchronized,而使用AtomicInteger來達到同樣的效果。

public class Task implements Runnable {
    //使用AtomicInteger初始化
    public static AtomicInteger count = new AtomicInteger(0);
    public void  increase(){
        //如下方法保證原子遞增
        count.incrementAndGet();
    }
    @Override
    public void run() {
        increase();
    }
}

AtomicInteger可以保證自增操作是原子性的。

注意
並不是有了原子性及可見性操作,就可以放棄使用鎖同步。原子性及可見性並不能保證線程安全,只有在一些特定的場景下才能夠達到避免使用鎖同步的效果,上面的樣例只是爲了說明Java提供的Atomvolatile功能,而特意設計的樣例場景。如果真實生產中想使用原子性及可見性替代鎖同步時,要認真分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章