原創:花括號MC(微信公衆號:huakuohao-mc)。關注JAVA基礎編程及大數據,注重經驗分享及個人成長。

這是併發編程系列的第三篇文章。 上一篇 介紹的是線程間通過鎖同步的方式實現共享資源的安全訪問,這篇講一下如何通過不加鎖的方式實現共享可變資源的訪問。

ThreadLocal介紹

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

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

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

ThreadLocal使用場景

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

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

原子性和可見性

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

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

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

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

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

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

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

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

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

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

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

結束

這篇文章介紹如何通過不使用鎖同步的情況,實現正確的併發訪問。至此,併發編程裏面兩種訪問共享可變資源的方式就都介紹完了。下一篇會介紹線程間的通信問題。

推薦閱讀:

1. Java併發編程那些事兒(一) ——任務與線程

2. Java8的Stream流真香,沒體驗過的永遠不知道

3. Awk這件上古神兵你會用了嗎

4. 手把手教你搭建一套ELK日誌搜索運維平臺

·END·

花括號MC

Java·大數據·個人成長

微信號:huakuohao-mc

點一下你會更好看耶

相關文章