Java併發編程之基礎篇(三) – ThreadLocal
ThreadLocal介紹
上篇文章講到,如果想在多線程的環境下,實現共享可變資源的安全訪問,最好的方式是加鎖,也就是同一時刻只有一個線程在使用共享可變資源。如果我們有一種方式可以根除對變量的共享,那麼就可以實現不加鎖的情況下對變量進行安全訪問。
還拿之前搶衛生間坑位的例子舉例,如果只有一個衛生間坑位,五個人都想去衛生間的話,那麼就需要加鎖同步。如果給每個人都提供一個單獨的坑位,那麼就可以不加鎖了,因爲沒有爭搶的場景發生。
Java通過ThreadLocal
來實現每個線程都擁有一份自己的共享變量的拷貝。大家可以把ThreadLocal<T>
簡單的理解成Map<Thread,T>
。ThreadLocal
提供了get
和set
等方法,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;
}
}
這段會啓動一個讀線程,當ready
爲true
時會打印出number
的值。然後主線程會修改ready
和number
的值。如果該段代碼是在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
提供的Atom
和volatile
功能,而特意設計的樣例場景。如果真實生產中想使用原子性及可見性替代鎖同步時,要認真分析。