自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

安全 應(yīng)用安全
compare and swap,解決多線程并行情況下使用鎖造成性能損耗的一種機(jī)制,CAS操作包含三個(gè)操作數(shù)——內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。

 概念

compare and swap,解決多線程并行情況下使用鎖造成性能損耗的一種機(jī)制,CAS操作包含三個(gè)操作數(shù)——內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會(huì)自動(dòng)將該位置值更新為新值。否則,處理器不做任何操作。無(wú)論哪種情況,它都會(huì)在CAS指令之前返回該位置的值。CAS有效地說(shuō)明了“我認(rèn)為位置V應(yīng)該包含值A(chǔ);如果包含該值,則將B放到這個(gè)位置;否則,不要更改該位置,只告訴我這個(gè)位置現(xiàn)在的值即可。

[[329907]]

簡(jiǎn)單點(diǎn)來(lái)說(shuō)就是修改之前先做一下對(duì)比,校驗(yàn)數(shù)據(jù)是否被其他線程修改過(guò),如果修改過(guò)了,那么將內(nèi)存中新的值取出在與內(nèi)存中的進(jìn)行對(duì)比,直到相等,然后再做修改。

假如我們要對(duì)變量:num做累加操作,num初始值=0。1.cpu前往內(nèi)存取出num;2.判斷內(nèi)存中的num是否被修改;3.做+1操作;4.將修改后的值寫(xiě)入內(nèi)存中;

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

這時(shí)候可能會(huì)有疑問(wèn)了,判斷、自加、寫(xiě)回內(nèi)存難道不會(huì)發(fā)生線程安全問(wèn)題嗎?既然cas能成為并發(fā)編程中安全問(wèn)題的解決這,那么這個(gè)問(wèn)題肯定是不會(huì)發(fā)生的,為什么呢?因?yàn)榕袛?、自加、?xiě)回內(nèi)存這是一個(gè)由硬件保證的原子操作,硬件是如何保證原子性的,請(qǐng)先看下面這個(gè)例子

需求:

使用三個(gè)線程分別對(duì)某個(gè)成員變量累加10W,打印累加結(jié)果。

我們使用兩種方法完成此需求。1.正常累加(既不加鎖,也不使用原子類(lèi))。

1.使用synchronized。

2.使用原子類(lèi)(Atomic)。

實(shí)現(xiàn)

1.正常累加(既不加鎖,也不使用原子類(lèi))。

這種方式?jīng)]有什么說(shuō)的,直接上代碼

  1. package com.ymy.test; 
  2.  
  3.  
  4. public class CASTest { 
  5.      
  6.     private static long count = 0; 
  7.  
  8.     /** 
  9.      * 累加10w 
  10.      */ 
  11.     private static  void add(){ 
  12.  
  13.         for (int i = 0; i< 100000; ++i){ 
  14.             count+=1; 
  15.         } 
  16.  
  17.     } 
  18.  
  19.     public static void main(String[] args) throws InterruptedException { 
  20.         //開(kāi)啟三個(gè)線程   t1   t2    t3 
  21.         Thread t1 = new Thread(() ->{ 
  22.             add(); 
  23.         }); 
  24.  
  25.         Thread t2 = new Thread(() ->{ 
  26.             add(); 
  27.         }); 
  28.  
  29.         Thread t3 = new Thread(() ->{ 
  30.             add(); 
  31.         }); 
  32.         long starTime = System.currentTimeMillis(); 
  33.         //啟動(dòng)三個(gè)線程 
  34.         t1.start(); 
  35.         t2.start(); 
  36.         t3.start(); 
  37.         //讓線程同步 
  38.         t1.join(); 
  39.         t2.join(); 
  40.         t3.join(); 
  41.         long endTime = System.currentTimeMillis(); 
  42.         System.out.println("累加完成,count:"+count); 
  43.         System.out.println("耗時(shí):"+(endTime - starTime)+" ms"); 
  44.     } 

執(zhí)行結(jié)果

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

很明顯,三個(gè)線程累加,由于cpu緩存的存在,導(dǎo)致結(jié)果遠(yuǎn)遠(yuǎn)小于30w,這個(gè)也是我們預(yù)期到的,所以才會(huì)出現(xiàn)后面兩種解決方案。

2.使用synchronized

使用synchronized時(shí)需要注意,需求要求我們?nèi)齻€(gè)線程分別累加10W,所以synchronized鎖定的內(nèi)容就非常重要了,要么直接鎖定類(lèi),要么三個(gè)線程使用同一把鎖,關(guān)于synchronized的介紹以及鎖定內(nèi)容請(qǐng)參考:java并發(fā)編程之synchronized

第一種,直接鎖定類(lèi),我這里采用鎖定靜態(tài)方法。

我們來(lái)改動(dòng)一下代碼,將add方法加上synchronized關(guān)鍵字即可,由于add方法已經(jīng)時(shí)靜態(tài)方法了,所以現(xiàn)在鎖定的時(shí)整個(gè)CASTest類(lèi)。

  1. /** 
  2.      * 累加10w 
  3.      */ 
  4.     private static synchronized   void add(){ 
  5.  
  6.         for (int i = 0; i< 100000; ++i){ 
  7.             count+=1; 
  8.         } 
  9.  
  10.     } 

運(yùn)行結(jié)果第一次:

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

第二次:

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

第三次:

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

這里就有意思了,加了鎖的運(yùn)行時(shí)間居然比不加鎖的運(yùn)行時(shí)間還少?是不是覺(jué)得有點(diǎn)不可思議了?其實(shí)這個(gè)也不難理解,這里就要牽扯到cpu緩存以及緩存與內(nèi)存的回寫(xiě)機(jī)制了,感興趣的小伙伴可以自行百度,今天的重點(diǎn)不在這里。

第二種:三個(gè)線程使用同一把鎖

改造代碼,去掉add方法的synchronized關(guān)鍵字,將synchronized寫(xiě)在add方法內(nèi),新建一把鑰匙(成員變量:lock),讓三個(gè)累加都累加操作使用這把鑰匙,代碼如下:

  1. package com.ymy.test; 
  2.  
  3.  
  4. public class CASTest { 
  5.  
  6.     private static long count = 0; 
  7.  
  8.  
  9.     private static final String lock = "lock"
  10.  
  11.     /** 
  12.      * 累加10w 
  13.      */ 
  14.     private static  void add() { 
  15.         synchronized(lock){ 
  16.             for (int i = 0; i < 100000; ++i) { 
  17.                 count += 1; 
  18.             } 
  19.         } 
  20.     } 
  21.  
  22.     public static void main(String[] args) throws InterruptedException { 
  23.         //開(kāi)啟三個(gè)線程   t1   t2    t3 
  24.         Thread t1 = new Thread(() -> { 
  25.             add(); 
  26.         }); 
  27.  
  28.         Thread t2 = new Thread(() -> { 
  29.             add(); 
  30.         }); 
  31.  
  32.         Thread t3 = new Thread(() -> { 
  33.             add(); 
  34.         }); 
  35.         long starTime = System.currentTimeMillis(); 
  36.         //啟動(dòng)三個(gè)線程 
  37.         t1.start(); 
  38.         t2.start(); 
  39.         t3.start(); 
  40.         //讓線程同步 
  41.         t1.join(); 
  42.         t2.join(); 
  43.         t3.join(); 
  44.         long endTime = System.currentTimeMillis(); 
  45.         System.out.println("累加完成,count:" + count); 
  46.         System.out.println("耗時(shí):" + (endTime - starTime) + " ms"); 
  47.     } 

結(jié)果如下:

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

這兩種加鎖方式都能保證線程的安全,但是這里你需要注意一點(diǎn),如果是在方法上加synchronized而不加static關(guān)鍵字的話,必須要保證多個(gè)線程共用這一個(gè)對(duì)象,否者加鎖無(wú)效。

原子類(lèi)

原子類(lèi)工具有很多,我們舉例的累加操作只用到其中的一種,我們一起看看java提供的原子工具有哪些:

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

工具類(lèi)還是很豐富的,我們結(jié)合需求來(lái)講解一下其中的一種,我們使用:AtomicLong。

AtomicLong提供了兩個(gè)構(gòu)造函數(shù):

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

value:原子操作的初始值,調(diào)用無(wú)參構(gòu)造value=0;調(diào)用有參構(gòu)造value=指定值

其中value還是被volatile 關(guān)鍵字修飾,volatile可以保證變量的可見(jiàn)性,什么叫可見(jiàn)性?可見(jiàn)性有一條很重要的規(guī)則:Happens-Before 規(guī)則,意思:前面一個(gè)操作的結(jié)果對(duì)后續(xù)操作是可見(jiàn)的,線程1對(duì)變量A的修改其他線程立馬可以看到,具體請(qǐng)自行百度。

我們接著來(lái)看累加的需求,AtomicLong提供了一個(gè)incrementAndGet(),源碼如下:

  1. /** 
  2.      * Atomically increments by one the current value. 
  3.      * 
  4.      * @return the updated value 
  5.      */ 
  6.     public final long incrementAndGet() { 
  7.         return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; 
  8.     } 

Atomically increments by one the current value :原子的增加一個(gè)當(dāng)前值。好了,我們現(xiàn)在試著將互斥鎖修改成原子類(lèi)工具,改造代碼:

1.實(shí)例化一個(gè)Long類(lèi)型的原子類(lèi)工具;

2.再for循環(huán)中使用incrementAndGet()方法進(jìn)行累加操作。

改造后的代碼:

  1. package com.ymy.test; 
  2.  
  3.  
  4. import java.util.concurrent.atomic.AtomicLong; 
  5.  
  6. public class CASTest { 
  7.  
  8. //    private static long count = 0; 
  9. // 
  10. //    private static final String lock = "lock"
  11.  
  12.     private static AtomicLong atomicLong = new AtomicLong(); 
  13.  
  14.     /** 
  15.      * 累加10w 
  16.      */ 
  17.     private static void add() { 
  18.         for (int i = 0; i < 100000; ++i) { 
  19.             atomicLong.incrementAndGet(); 
  20.         } 
  21.     } 
  22.  
  23.     public static void main(String[] args) throws InterruptedException { 
  24.         //開(kāi)啟三個(gè)線程   t1   t2    t3 
  25.         Thread t1 = new Thread(() -> { 
  26.             add(); 
  27.         }); 
  28.  
  29.         Thread t2 = new Thread(() -> { 
  30.             add(); 
  31.         }); 
  32.  
  33.         Thread t3 = new Thread(() -> { 
  34.             add(); 
  35.         }); 
  36.         long starTime = System.currentTimeMillis(); 
  37.         //啟動(dòng)三個(gè)線程 
  38.         t1.start(); 
  39.         t2.start(); 
  40.         t3.start(); 
  41.         //讓線程同步 
  42.         t1.join(); 
  43.         t2.join(); 
  44.         t3.join(); 
  45.         long endTime = System.currentTimeMillis(); 
  46.         //System.out.println("累加完成,count:" + count); 
  47.         System.out.println("累加完成,count:" + atomicLong); 
  48.         System.out.println("耗時(shí):" + (endTime - starTime) + " ms"); 
  49.     } 

結(jié)果:

 

請(qǐng)問(wèn)在不加鎖的情況下如何保證線程安全?

 

可以得到累加的結(jié)果也是:30w,但時(shí)間卻比互斥鎖要久,這是為什么呢?我們一起來(lái)解剖一下源碼。

AtomicLong incrementAndGet()源碼解析

  1. /** 
  2.      * Atomically increments by one the current value. 
  3.      * 
  4.      * @return the updated value 
  5.      */ 
  6.     public final long incrementAndGet() { 
  7.         return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; 
  8.     } 
  9.  
  10.     public final long getAndAddLong(Object var1, long var2, long var4) { 
  11.         long var6; 
  12.         do { 
  13.             var6 = this.getLongVolatile(var1, var2); 
  14.         } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4)); 
  15.  
  16.         return var6; 
  17.     } 

我們來(lái)看看getAndAddLong方法,發(fā)現(xiàn)內(nèi)部使用了一個(gè) do while 循環(huán),我們看看循環(huán)的條件是什么

  1. public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6); 

這是循環(huán)條件的源碼,不知道你們發(fā)現(xiàn)沒(méi)有一個(gè)關(guān)鍵字:native,表示java代碼已經(jīng)走完了,這里需要調(diào)用C/C++代碼,這里調(diào)用的時(shí)C++代碼,在這里就要解釋一下為什么原子類(lèi)的比較和賦值是線程安全的,那是因?yàn)閏++代碼中是有加鎖的,不知道你們是否了解過(guò)內(nèi)存與cpu的消息總線制,c++就是再消息總線中加了lock,保證了互斥性,所以對(duì)比和賦值是一個(gè)原子操作,線程安全的。

Unsafe,這個(gè)類(lèi)可以為原子工具類(lèi)提供硬件級(jí)別的原子性,雖然我們java中使用的這些原子工具類(lèi)雖然都是無(wú)鎖的,但是我們無(wú)需考慮他的多線程安全問(wèn)題。

為什么原子類(lèi)比互斥鎖的效率低?

好了,現(xiàn)在來(lái)思考一下為什么原子工具類(lèi)的效率會(huì)比互斥鎖低?明明沒(méi)有加鎖,反而比加了鎖慢,這是不是有點(diǎn)不合常理?其實(shí)這很符合常理,我們一起來(lái)分析一波,CAS(Compare and Swap)重在比較,我們看源碼的時(shí)候發(fā)現(xiàn)有一個(gè) do while循環(huán),這個(gè)循環(huán)的作用是什么呢?

1.判斷期望值是否和內(nèi)存中的值一致;

2.如果不一致,獲取內(nèi)存中最新的值(var6),此時(shí)期望值就等于了var6,使用改期望值繼續(xù)與內(nèi)存中的值做對(duì)比,直到發(fā)現(xiàn)期望值和內(nèi)存中的值一致,+1之后返回結(jié)果。

這里有一個(gè)問(wèn)題不知道你們發(fā)現(xiàn)沒(méi)有,就是這個(gè)循環(huán)問(wèn)題,1.我們假設(shè)線程1最先訪問(wèn)內(nèi)存中的num值=0;加載到cpu中;2.還沒(méi)有做累加操作,cpu執(zhí)行了線程切換操作;3.線程2得到了使用權(quán),線程2也去內(nèi)存中加載num=0,累加之后將結(jié)果返回到了內(nèi)存中;4.線程切回線程1,接著上面的操作,要和內(nèi)存中的num進(jìn)行對(duì)比,期望值=0,內(nèi)存num=1,法向?qū)Ρ炔簧?,從新獲取內(nèi)存中的num=1加載到線程1所在的cpu中;5.此時(shí)線程又切換了,這次切換了線程3;6.線程3從內(nèi)存中加載num=1到線程3所在的cpu中,之后拿著期望值=1與內(nèi)存中的num=1做對(duì)比,發(fā)現(xiàn)值并沒(méi)有被修改,此時(shí),累加結(jié)果之后寫(xiě)回內(nèi)存;7.線程1拿到使用權(quán);8.線程1期望值=1與內(nèi)存num=2做對(duì)比,發(fā)現(xiàn)又不相同,此時(shí)又需要將內(nèi)存中的新num=3加載到線程1所在的cpu中,然后拿著期望值=2與內(nèi)存num=2做對(duì)比,發(fā)現(xiàn)相同,累加將結(jié)果寫(xiě)回內(nèi)存。

這是再多線程下的一種情況,我們發(fā)現(xiàn)線程1做了兩次對(duì)比,而真正的程序循環(huán)對(duì)比的次數(shù)肯定會(huì)比我們分析的多,互斥鎖三個(gè)線程累加10w,只需要累加30萬(wàn)次即可,而原子類(lèi)工具需要累加30萬(wàn)次并且循環(huán)很多次,可能幾千次,也可能幾十萬(wàn)次,所以再內(nèi)存中累加操作互斥鎖會(huì)比原子類(lèi)效率高,因?yàn)閮?nèi)存的執(zhí)行效率高,會(huì)導(dǎo)致一個(gè)對(duì)比執(zhí)行很多循環(huán),我們稱(chēng)這個(gè)循環(huán)叫:自旋。

是不是所有情況下都是互斥鎖要快呢?肯定不是的,如果操作的數(shù)據(jù)再磁盤(pán)中,或者操作數(shù)據(jù)量太多時(shí),原子類(lèi)就會(huì)比互斥鎖的性能高很多,這很好理解,就像內(nèi)存中單線程比多線程效率會(huì)更高(一般情況)。

CAS的ABA問(wèn)題

ABA是什么?我們來(lái)舉個(gè)例子:變量a初始值=0,被線程1獲取a=0,切換到線程2,獲取a=0,并且將a修改為1寫(xiě)回內(nèi)存,切換到線程3,再內(nèi)存中獲取數(shù)據(jù)a=1,將數(shù)據(jù)修改為0然后寫(xiě)回內(nèi)存,切換到線程1,這時(shí)候線程1發(fā)現(xiàn)內(nèi)存中的值還是0,線程1認(rèn)為內(nèi)存中a沒(méi)有被修改,這時(shí)候線程1將a的值修改為1,寫(xiě)回內(nèi)存。

我們來(lái)分析一下這波操作會(huì)不會(huì)有風(fēng)險(xiǎn),從表面上看,好像沒(méi)什么問(wèn)題,累加或者值修改的時(shí)候問(wèn)題不大,覺(jué)得這個(gè)ABA沒(méi)有什么風(fēng)險(xiǎn),如果你這樣認(rèn)為,那就大錯(cuò)特錯(cuò)了,我舉個(gè)例子,用戶(hù)A用網(wǎng)上銀行給用戶(hù)B轉(zhuǎn)錢(qián),同時(shí)用戶(hù)C也在給用戶(hù)A轉(zhuǎn)錢(qián),我們假設(shè)用戶(hù)A賬戶(hù)余額100元,用戶(hù)A要給用戶(hù)B轉(zhuǎn)100元,用戶(hù)C要給用戶(hù)A轉(zhuǎn)100元,用戶(hù)A轉(zhuǎn)給用戶(hù)B、用戶(hù)C轉(zhuǎn)給用戶(hù)A同時(shí)發(fā)生,但由于用戶(hù)A的網(wǎng)絡(luò)不好,用戶(hù)A點(diǎn)了一下之后沒(méi)有反應(yīng),接著又點(diǎn)了一下,這時(shí)候就會(huì)發(fā)送兩條用戶(hù)A給用戶(hù)B轉(zhuǎn)100元的請(qǐng)求。

我們假設(shè)線程1:用戶(hù)A第一次轉(zhuǎn)用戶(hù)B100元

線程2:用戶(hù)A第二次轉(zhuǎn)用戶(hù)B100元

線程3:用戶(hù)C轉(zhuǎn)用戶(hù)A100元。

線程1執(zhí)行的時(shí)候獲取用戶(hù)A的余額=100元,此時(shí)切換到了線程2,也獲取到了用戶(hù)A的余額=100元,線程2做了扣錢(qián)操作(update money-100 where money=100),100是我們剛查出來(lái)的,扣完之后余額應(yīng)該變成了0元,切換到線程3,用戶(hù)C轉(zhuǎn)給用戶(hù)A100元,此時(shí)用戶(hù)A的賬戶(hù)又變成了100元,切換到線程1,執(zhí)行扣錢(qián)操作(update money-100 where money=100),本來(lái)是應(yīng)該扣錢(qián)失敗的,由于用戶(hù)C給用戶(hù)A轉(zhuǎn)了100元,導(dǎo)致用戶(hù)A的余額又變成了100元,所以線程1也扣錢(qián)成功了。

這是不是很恐怖?所以在開(kāi)發(fā)的時(shí)候,ABA問(wèn)題是否需要注意,還請(qǐng)分析好應(yīng)用場(chǎng)景,像之前說(shuō)的這個(gè)ABA問(wèn)題,數(shù)據(jù)庫(kù)層面我們可以加版本號(hào)(版本號(hào)累加)就能解決,程序中原子類(lèi)也給我們提供了解決方案:AtomicStampedReference,感興趣的小伙伴可以研究一下。其實(shí)思路和版本號(hào)類(lèi)似,比較的時(shí)候不僅需要比較期望值,還要對(duì)比版本號(hào),都相同的情況下才會(huì)做修改。

 

責(zé)任編輯:武曉燕 來(lái)源: 今日頭條
相關(guān)推薦

2023-03-02 08:19:43

不加鎖程序實(shí)時(shí)性

2021-11-12 07:21:51

Go線程安全

2023-10-26 07:32:42

2020-06-24 13:08:14

網(wǎng)絡(luò)安全網(wǎng)絡(luò)安全技術(shù)周刊

2024-06-17 00:02:00

線程安全HashMapJDK 1.7

2023-01-26 02:07:51

HashSet線程安全

2024-05-20 13:13:01

線程安全Java

2022-07-05 08:41:56

數(shù)據(jù)安全工具安全備份

2022-09-16 08:42:23

JavaAPI變量

2023-03-27 13:00:13

Javascript前端

2010-06-30 10:55:13

SQL Server日

2019-09-03 09:55:48

DevOps云計(jì)算安全

2021-10-26 15:59:18

WiFi 6WiFi 5通信網(wǎng)絡(luò)

2019-12-12 15:32:48

ITvCenterVMware

2017-06-02 08:48:29

互斥鎖JavaCAS

2015-06-01 06:39:18

JavaJava比C++

2020-11-18 09:26:52

@property裝飾器代碼

2016-12-01 18:57:39

火狐瀏覽器Firefox

2024-01-09 11:39:47

數(shù)字化轉(zhuǎn)型數(shù)字優(yōu)先企業(yè)

2019-02-27 12:00:09

開(kāi)源Org模式Emacs
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)