摘要:例子先來看下面的示例來驗證下到底是不是線程安全的。上面的例子我們期望的結果應該是,但運行遍,你會發現總是不為,至少你現在知道了操作它不是線程安全的了。它的性能比較好也是因為避免了使線程進入內核態的阻塞狀態。
例子
先來看下面的示例來驗證下 i++ 到底是不是線程安全的。
1000個線程,每個線程對共享變量 count 進行 1000 次 ++ 操作。
上面的例子我們期望的結果應該是 1000000,但運行 N 遍,你會發現總是不為 1000000,至少你現在知道了 i++
操作它不是線程安全的了。
每個線程都有自己的工作內存,每個線程需要對共享變量操作時必須先把共享變量從主內存 load 到自己的工作內存,等完成對共享變量的操作時再 save 到主內存。
問題就出在這了,如果一個線程運算完后還沒刷到主內存,此時這個共享變量的值被另外一個線程從主內存讀取到了,這個時候讀取的數據就是臟數據了,它會覆蓋其他線程計算完的值。。。
這也是經典的內存不可見問題,那么把 count 加上 volatile 讓內存可見是否能解決這個問題呢? 答案是:不能。因為
volatile 只能保證可見性,不能保證原子性。多個線程同時讀取這個共享變量的值,就算保證其他線程修改的可見性,也不能保證線程之間讀取到同樣的值然后相互覆蓋對方的值的情況。
關于多線程的幾種關鍵概念請翻閱《多線程之原子性、可見性、有序性詳解》這篇文章。
解決方案說了這么多,對于 i++ 這種線程不安全問題有沒有其他解決方案呢?當然有,請參考以下幾種解決方案。
1、對 i++ 操作的方法加同步鎖,同時只能有一個線程執行 i++ 操作;
2、使用支持原子性操作的類,如 java.util.concurrent.atomic.AtomicInteger,它使用的是
CAS 算法,效率優于第 1 種;
CAS:Compare and Swap, 翻譯成比較并交換。鏈接描述
java.util.concurrent包中借助CAS實現了區別于synchronouse同步鎖的一種樂觀鎖,使用這些類在多核CPU的機器上會有比較好的性能.
CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。
今天我們主要是針對AtomicInteger的incrementAndGet做深入分析。
/** * Atomically increments by one the current value. * * @return the updated value */ public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
循環的內容是:
1.取得當前值
2.計算+1后的值
3.如果當前值沒有被覆蓋的話設置那個+1后的值
4.如果設置沒成功, 再從1開始
在這個方法中可以看到compareAndSet這個方法,我們進入看一下。
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return true if successful. False return indicates that * the actual value was not equal to the expected value. */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
調用UnSafe這個類的compareAndSwapInt
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
JAVA程序也就跟蹤到這里為止了,剩下的就是通過JNI調用C程序了,可是我奇怪的是為什么變量名都是var1,var2這樣的命名呢?JAVA編程規范不是說不使用1,2等沒有含義的字符命名嗎?
JNI原生實現部分
在openJDK中找到找到unsafe.cpp這個文件,代碼如下:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END
核心方法是compxchg,這個方法所屬的類文件是在OS_CPU目錄下面,由此可以看出這個類是和CPU操作有關,進入代碼如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } }
這個方法里面都是匯編指命,看到LOCK_IF_MP也有鎖指令實現的原子操作,其實CAS也算是有鎖操作,只不過是由CPU來觸發,比synchronized性能好的多。
使用cas的類ReenTrantLock、countDownLatch、AtomicInteger
ReenTrantLock和synchronized比較可重入性:
從名字上理解,ReenTrantLock的字面意思就是再進入的鎖,其實synchronized關鍵字所使用的鎖也是可重入的,兩者關于這個的區別不大。兩者都是同一個線程沒進入一次,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。
鎖的實現:
Synchronized是依賴于JVM實現的,而ReenTrantLock是JDK實現的,有什么區別,說白了就類似于操作系統來控制實現和用戶自己敲代碼實現的區別。前者的實現是比較難見到的,后者有直接的源碼可供閱讀。
性能的區別:
在Synchronized優化以前,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)后,兩者的性能就差不多了,在兩種方法都可用的情況下,官方甚至建議使用synchronized,其實synchronized的優化我感覺就借鑒了ReenTrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。
功能區別:
便利性:很明顯Synchronized的使用比較方便簡潔,并且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工聲明來加鎖和釋放鎖,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中聲明釋放鎖。
鎖的細粒度和靈活度:很明顯ReenTrantLock優于Synchronized
ReenTrantLock獨有的能力:
1.ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
2.ReenTrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的線程們,而不是像synchronized要么隨機喚醒一個線程要么喚醒全部線程。
3.ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現這個機制。
ReenTrantLock實現的原理:
在網上看到相關的源碼分析,本來這塊應該是本文的核心,但是感覺比較復雜就不一一詳解了,簡單來說,ReenTrantLock的實現是一種自旋鎖,通過循環調用CAS(無鎖算法)操作來實現加鎖。它的性能比較好也是因為避免了使線程進入內核態的阻塞狀態。想盡辦法避免線程進入內核的阻塞狀態是我們去分析和理解鎖設計的關鍵鑰匙。
1、對于資源競爭較少的情況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗cpu資源;而CAS基于硬件實現,不需要進入內核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。
2、對于資源競爭嚴重的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低于synchronized。以java.util.concurrent.atomic包中AtomicInteger類為例,其getAndIncrement()方法實現如下:
如果compareAndSet(current, next)方法成功執行,則直接返回;如果線程競爭激烈,導致compareAndSet(current, next)方法一直不能成功執行,則會一直循環等待,直到耗盡cpu分配給該線程的時間片,從而大幅降低效率。
java共享鎖實現原理及CountDownLatch解析 鏈接描述 CountDownLatch使用解說例子1: CountDownLatch是java5中新增的一個并發工具類,其使用非常簡單,下面通過偽代碼簡單看一下使用方式:
這是一個使用CountDownLatch非常簡單的例子,創建的時候,需要指定一個初始狀態值,本例為2,主線程調用 latch.await時,除非latch狀態值為0,否則會一直阻塞休眠。當所有任務執行完后,主線程喚醒,最終執行打印動作。
例子2:以上只是一個最簡單的例子,接著咱們再來看一個,這回,咱們想要在任務執行完后做更多的事情,如下圖所示:
這一次,在線程3和線程4中,分別調用了latch.await(),當latch狀態值為0時,這兩個線程將會繼續執行任務,但是順序性是無法保證的。
CountDownLatch的方便之處在于,你可以在一個線程中使用,也可以在多個線程上使用,一切只依據狀態值,這樣便不會受限于任何的場景。
java共享鎖模型在java5提供的并發包下,有一個AbstractQueuedSynchronizer抽象類,也叫AQS,此類根據大部分并發共性作了一些抽象,便于開發者實現如排他鎖,共享鎖,條件等待等更高級的業務功能。它通過使用CAS和隊列模型,出色的完成了抽象任務,在此向Doug Lea致敬。
AQS比較抽象,并且是優化精簡的代碼,如果一頭扎進去,可能會比較容易迷失。本篇只解說CountDownLatch中使用到的共享鎖模型。
我們以CountDownLatch第二個例子作為案例來分析一下,一開始,我們創建了一個CountDownLatch實例,
此時,AQS中,狀態值state=2,對于 CountDownLatch 來說,state=2表示所有調用await方法的線程都應該阻塞,等到同一個latch被調用兩次countDown后才能喚醒沉睡的線程。接著線程3和線程4執行了 await方法,這會的狀態圖如下:
注意,上面的通知狀態是節點的屬性,表示該節點出隊后,必須喚醒其后續的節點線程。當線程1和線程2分別執行完latch.countDown方法后,會把state值置為0,此時,通過CAS成功置為0的那個線程將會同時承擔起喚醒隊列中第一個節點線程的任務,從上圖可以看出,第一個節點即為線程3,當線程3恢復執行之后,其發現狀態值為通知狀態,所以會喚醒后續節點,即線程4節點,然后線程3繼續做自己的事情,到這里,線程3和線程4都已經被喚醒,CountDownLatch功成身退。
上面的流程,如果落實到代碼,把 state置為0的那個線程,會判斷head指向節點的狀態,如果為通知狀態,則喚醒后續節點,即線程3節點,然后head指向線程3節點,head指向的舊節點會被刪除掉。當線程3恢復執行后,發現自身為通知狀態,又會把head指向線程4節點,然后刪除自身節點,并喚醒
線程4。
這里可能讀者會有個疑問,線程節點的狀態是什么時候設置上去的。其實,一個線程在阻塞之前,就會把它前面的節點設置為通知狀態,這樣便可以實現鏈式喚醒機制了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71402.html
摘要:但是有引入了新的問題線程不安全,返回的對象可能還沒有初始化。如果只有一個線程調用是沒有問題的因為不管步驟如何調換,保證返回的對象是已經構造好了。這種特殊情況稱之為指令重排序采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理。 目錄 雙重檢測鎖的演變過程 利用HappensBefore分析并發問題 無volatile的雙重檢測鎖 雙重檢測鎖的演變過程 synch...
摘要:今天主要講解的是本文力求簡單講清每個知識點,希望大家看完能有所收獲一和回顧線程安全的和我們知道是用于替代的,是線程安全的容器。使用迭代器遍歷時不需要顯示加鎖,看看與方法的實現可能就有點眉目了。 前言 只有光頭才能變強 showImg(https://segmentfault.com/img/remote/1460000016931828?w=1120&h=640); 前一陣子寫過一篇C...
摘要:嚴格來說,并不是單線程的。其他異步和事件驅動相關的線程通過來實現內部的線程池和線程調度。線程是最小的進程,因此也是單進程的。子進程中執行的是非程序,提供一組參數后,執行的結果以回調的形式返回。在子進程中通過和的機制來接收和發送消息。 ??node遵循的是單線程單進程的模式,node的單線程是指js的引擎只有一個實例,且在nodejs的主線程中執行,同時node以事件驅動的方式處理IO...
摘要:懶漢非線程安全,需要用一定的風騷操作控制,裝逼失敗有可能導致看一周的海綿寶寶餓漢天生線程安全,的時候就已經實例化好,該操作過于風騷會造成資源浪費單例注冊表初始化的時候,默認單例用的就是該方式特點私有構造方法,只能有一個實例。 單例設計模式(Singleton Pattern)是最簡單且常見的設計模式之一,主要作用是提供一個全局訪問且只實例化一次的對象,避免多實例對象的情況下引起邏輯性錯...
摘要:如何在線程池中提交線程內存模型相關問題什么是的內存模型,中各個線程是怎么彼此看到對方的變量的請談談有什么特點,為什么它能保證變量對所有線程的可見性既然能夠保證線程間的變量可見性,是不是就意味著基于變量的運算就是并發安全的請對比下對比的異同。 并發編程高級面試面試題 showImg(https://upload-images.jianshu.io/upload_images/133416...
閱讀 1211·2023-04-25 20:31
閱讀 3718·2021-10-14 09:42
閱讀 1485·2021-09-22 16:06
閱讀 2636·2021-09-10 10:50
閱讀 3524·2021-09-07 10:19
閱讀 1772·2019-08-30 15:53
閱讀 1170·2019-08-29 15:13
閱讀 2818·2019-08-29 13:20