shiro如何保证session不失效_请问在不加锁的情况下如何保证线程安全?
概念
compare and swap,解決多線程并行情況下使用鎖造成性能損耗的一種機制,CAS操作包含三個操作數(shù)——內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認為位置V應(yīng)該包含值A(chǔ);如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現(xiàn)在的值即可。
簡單點來說就是修改之前先做一下對比,校驗數(shù)據(jù)是否被其他線程修改過,如果修改過了,那么將內(nèi)存中新的值取出在與內(nèi)存中的進行對比,直到相等,然后再做修改。
假如我們要對變量:num做累加操作,num初始值=0。1.cpu前往內(nèi)存取出num;2.判斷內(nèi)存中的num是否被修改;3.做+1操作;4.將修改后的值寫入內(nèi)存中;
這時候可能會有疑問了,判斷、自加、寫回內(nèi)存難道不會發(fā)生線程安全問題嗎?既然cas能成為并發(fā)編程中安全問題的解決這,那么這個問題肯定是不會發(fā)生的,為什么呢?因為判斷、自加、寫回內(nèi)存這是一個由硬件保證的原子操作,硬件是如何保證原子性的,請先看下面這個例子
需求:
使用三個線程分別對某個成員變量累加10W,打印累加結(jié)果。
我們使用兩種方法完成此需求。1.正常累加(既不加鎖,也不使用原子類)。1.使用synchronized。
2.使用原子類(Atomic)。
實現(xiàn)
1.正常累加(既不加鎖,也不使用原子類)。
這種方式?jīng)]有什么說的,直接上代碼
package com.ymy.test;public class CASTest { private static long count = 0; /** * 累加10w */ private static void add(){ for (int i = 0; i< 100000; ++i){ count+=1; } } public static void main(String[] args) throws InterruptedException { //開啟三個線程 t1 t2 t3 Thread t1 = new Thread(() ->{ add(); }); Thread t2 = new Thread(() ->{ add(); }); Thread t3 = new Thread(() ->{ add(); }); long starTime = System.currentTimeMillis(); //啟動三個線程 t1.start(); t2.start(); t3.start(); //讓線程同步 t1.join(); t2.join(); t3.join(); long endTime = System.currentTimeMillis(); System.out.println("累加完成,count:"+count); System.out.println("耗時:"+(endTime - starTime)+" ms"); }}執(zhí)行結(jié)果
很明顯,三個線程累加,由于cpu緩存的存在,導致結(jié)果遠遠小于30w,這個也是我們預(yù)期到的,所以才會出現(xiàn)后面兩種解決方案。
2.使用synchronized
使用synchronized時需要注意,需求要求我們?nèi)齻€線程分別累加10W,所以synchronized鎖定的內(nèi)容就非常重要了,要么直接鎖定類,要么三個線程使用同一把鎖,關(guān)于synchronized的介紹以及鎖定內(nèi)容請參考:java并發(fā)編程之synchronized
第一種,直接鎖定類,我這里采用鎖定靜態(tài)方法。
我們來改動一下代碼,將add方法加上synchronized關(guān)鍵字即可,由于add方法已經(jīng)時靜態(tài)方法了,所以現(xiàn)在鎖定的時整個CASTest類。
/** * 累加10w */ private static synchronized void add(){ for (int i = 0; i< 100000; ++i){ count+=1; } }運行結(jié)果第一次:
第二次:
第三次:
這里就有意思了,加了鎖的運行時間居然比不加鎖的運行時間還少?是不是覺得有點不可思議了?其實這個也不難理解,這里就要牽扯到cpu緩存以及緩存與內(nèi)存的回寫機制了,感興趣的小伙伴可以自行百度,今天的重點不在這里。
第二種:三個線程使用同一把鎖
改造代碼,去掉add方法的synchronized關(guān)鍵字,將synchronized寫在add方法內(nèi),新建一把鑰匙(成員變量:lock),讓三個累加都累加操作使用這把鑰匙,代碼如下:
package com.ymy.test;public class CASTest { private static long count = 0; private static final String lock = "lock"; /** * 累加10w */ private static void add() { synchronized(lock){ for (int i = 0; i < 100000; ++i) { count += 1; } } } public static void main(String[] args) throws InterruptedException { //開啟三個線程 t1 t2 t3 Thread t1 = new Thread(() -> { add(); }); Thread t2 = new Thread(() -> { add(); }); Thread t3 = new Thread(() -> { add(); }); long starTime = System.currentTimeMillis(); //啟動三個線程 t1.start(); t2.start(); t3.start(); //讓線程同步 t1.join(); t2.join(); t3.join(); long endTime = System.currentTimeMillis(); System.out.println("累加完成,count:" + count); System.out.println("耗時:" + (endTime - starTime) + " ms"); }}結(jié)果如下:
這兩種加鎖方式都能保證線程的安全,但是這里你需要注意一點,如果是在方法上加synchronized而不加static關(guān)鍵字的話,必須要保證多個線程共用這一個對象,否者加鎖無效。
原子類
原子類工具有很多,我們舉例的累加操作只用到其中的一種,我們一起看看java提供的原子工具有哪些:
工具類還是很豐富的,我們結(jié)合需求來講解一下其中的一種,我們使用:AtomicLong。
AtomicLong提供了兩個構(gòu)造函數(shù):
value:原子操作的初始值,調(diào)用無參構(gòu)造value=0;調(diào)用有參構(gòu)造value=指定值
其中value還是被volatile 關(guān)鍵字修飾,volatile可以保證變量的可見性,什么叫可見性?可見性有一條很重要的規(guī)則:Happens-Before 規(guī)則,意思:前面一個操作的結(jié)果對后續(xù)操作是可見的,線程1對變量A的修改其他線程立馬可以看到,具體請自行百度。
我們接著來看累加的需求,AtomicLong提供了一個incrementAndGet(),源碼如下:
/** * Atomically increments by one the current value. * * @return the updated value */ public final long incrementAndGet() { return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; } Atomically increments by one the current value :原子的增加一個當前值。好了,我們現(xiàn)在試著將互斥鎖修改成原子類工具,改造代碼:1.實例化一個Long類型的原子類工具;
2.再for循環(huán)中使用incrementAndGet()方法進行累加操作。
改造后的代碼:
package com.ymy.test;import java.util.concurrent.atomic.AtomicLong;public class CASTest {// private static long count = 0; private static final String lock = "lock"; private static AtomicLong atomicLong = new AtomicLong(); /** * 累加10w */ private static void add() { for (int i = 0; i < 100000; ++i) { atomicLong.incrementAndGet(); } } public static void main(String[] args) throws InterruptedException { //開啟三個線程 t1 t2 t3 Thread t1 = new Thread(() -> { add(); }); Thread t2 = new Thread(() -> { add(); }); Thread t3 = new Thread(() -> { add(); }); long starTime = System.currentTimeMillis(); //啟動三個線程 t1.start(); t2.start(); t3.start(); //讓線程同步 t1.join(); t2.join(); t3.join(); long endTime = System.currentTimeMillis(); //System.out.println("累加完成,count:" + count); System.out.println("累加完成,count:" + atomicLong); System.out.println("耗時:" + (endTime - starTime) + " ms"); }}結(jié)果:
可以得到累加的結(jié)果也是:30w,但時間卻比互斥鎖要久,這是為什么呢?我們一起來解剖一下源碼。
AtomicLong incrementAndGet()源碼解析
/** * Atomically increments by one the current value. * * @return the updated value */ public final long incrementAndGet() { return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; } public final long getAndAddLong(Object var1, long var2, long var4) { long var6; do { var6 = this.getLongVolatile(var1, var2); } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4)); return var6; }我們來看看getAndAddLong方法,發(fā)現(xiàn)內(nèi)部使用了一個 do while 循環(huán),我們看看循環(huán)的條件是什么
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);這是循環(huán)條件的源碼,不知道你們發(fā)現(xiàn)沒有一個關(guān)鍵字:native,表示java代碼已經(jīng)走完了,這里需要調(diào)用C/C++代碼,這里調(diào)用的時C++代碼,在這里就要解釋一下為什么原子類的比較和賦值是線程安全的,那是因為c++代碼中是有加鎖的,不知道你們是否了解過內(nèi)存與cpu的消息總線制,c++就是再消息總線中加了lock,保證了互斥性,所以對比和賦值是一個原子操作,線程安全的。
Unsafe,這個類可以為原子工具類提供硬件級別的原子性,雖然我們java中使用的這些原子工具類雖然都是無鎖的,但是我們無需考慮他的多線程安全問題。
為什么原子類比互斥鎖的效率低?
好了,現(xiàn)在來思考一下為什么原子工具類的效率會比互斥鎖低?明明沒有加鎖,反而比加了鎖慢,這是不是有點不合常理?其實這很符合常理,我們一起來分析一波,CAS(Compare and Swap)重在比較,我們看源碼的時候發(fā)現(xiàn)有一個 do while循環(huán),這個循環(huán)的作用是什么呢?
1.判斷期望值是否和內(nèi)存中的值一致;
2.如果不一致,獲取內(nèi)存中最新的值(var6),此時期望值就等于了var6,使用改期望值繼續(xù)與內(nèi)存中的值做對比,直到發(fā)現(xiàn)期望值和內(nèi)存中的值一致,+1之后返回結(jié)果。
這里有一個問題不知道你們發(fā)現(xiàn)沒有,就是這個循環(huán)問題,1.我們假設(shè)線程1最先訪問內(nèi)存中的num值=0;加載到cpu中;2.還沒有做累加操作,cpu執(zhí)行了線程切換操作;3.線程2得到了使用權(quán),線程2也去內(nèi)存中加載num=0,累加之后將結(jié)果返回到了內(nèi)存中;4.線程切回線程1,接著上面的操作,要和內(nèi)存中的num進行對比,期望值=0,內(nèi)存num=1,法向?qū)Ρ炔簧?#xff0c;從新獲取內(nèi)存中的num=1加載到線程1所在的cpu中;5.此時線程又切換了,這次切換了線程3;6.線程3從內(nèi)存中加載num=1到線程3所在的cpu中,之后拿著期望值=1與內(nèi)存中的num=1做對比,發(fā)現(xiàn)值并沒有被修改,此時,累加結(jié)果之后寫回內(nèi)存;7.線程1拿到使用權(quán);8.線程1期望值=1與內(nèi)存num=2做對比,發(fā)現(xiàn)又不相同,此時又需要將內(nèi)存中的新num=3加載到線程1所在的cpu中,然后拿著期望值=2與內(nèi)存num=2做對比,發(fā)現(xiàn)相同,累加將結(jié)果寫回內(nèi)存。
這是再多線程下的一種情況,我們發(fā)現(xiàn)線程1做了兩次對比,而真正的程序循環(huán)對比的次數(shù)肯定會比我們分析的多,互斥鎖三個線程累加10w,只需要累加30萬次即可,而原子類工具需要累加30萬次并且循環(huán)很多次,可能幾千次,也可能幾十萬次,所以再內(nèi)存中累加操作互斥鎖會比原子類效率高,因為內(nèi)存的執(zhí)行效率高,會導致一個對比執(zhí)行很多循環(huán),我們稱這個循環(huán)叫:自旋。
是不是所有情況下都是互斥鎖要快呢?肯定不是的,如果操作的數(shù)據(jù)再磁盤中,或者操作數(shù)據(jù)量太多時,原子類就會比互斥鎖的性能高很多,這很好理解,就像內(nèi)存中單線程比多線程效率會更高(一般情況)。
CAS的ABA問題
ABA是什么?我們來舉個例子:變量a初始值=0,被線程1獲取a=0,切換到線程2,獲取a=0,并且將a修改為1寫回內(nèi)存,切換到線程3,再內(nèi)存中獲取數(shù)據(jù)a=1,將數(shù)據(jù)修改為0然后寫回內(nèi)存,切換到線程1,這時候線程1發(fā)現(xiàn)內(nèi)存中的值還是0,線程1認為內(nèi)存中a沒有被修改,這時候線程1將a的值修改為1,寫回內(nèi)存。
我們來分析一下這波操作會不會有風險,從表面上看,好像沒什么問題,累加或者值修改的時候問題不大,覺得這個ABA沒有什么風險,如果你這樣認為,那就大錯特錯了,我舉個例子,用戶A用網(wǎng)上銀行給用戶B轉(zhuǎn)錢,同時用戶C也在給用戶A轉(zhuǎn)錢,我們假設(shè)用戶A賬戶余額100元,用戶A要給用戶B轉(zhuǎn)100元,用戶C要給用戶A轉(zhuǎn)100元,用戶A轉(zhuǎn)給用戶B、用戶C轉(zhuǎn)給用戶A同時發(fā)生,但由于用戶A的網(wǎng)絡(luò)不好,用戶A點了一下之后沒有反應(yīng),接著又點了一下,這時候就會發(fā)送兩條用戶A給用戶B轉(zhuǎn)100元的請求。
我們假設(shè)線程1:用戶A第一次轉(zhuǎn)用戶B100元
線程2:用戶A第二次轉(zhuǎn)用戶B100元
線程3:用戶C轉(zhuǎn)用戶A100元。
線程1執(zhí)行的時候獲取用戶A的余額=100元,此時切換到了線程2,也獲取到了用戶A的余額=100元,線程2做了扣錢操作(update money-100 where money=100),100是我們剛查出來的,扣完之后余額應(yīng)該變成了0元,切換到線程3,用戶C轉(zhuǎn)給用戶A100元,此時用戶A的賬戶又變成了100元,切換到線程1,執(zhí)行扣錢操作(update money-100 where money=100),本來是應(yīng)該扣錢失敗的,由于用戶C給用戶A轉(zhuǎn)了100元,導致用戶A的余額又變成了100元,所以線程1也扣錢成功了。
這是不是很恐怖?所以在開發(fā)的時候,ABA問題是否需要注意,還請分析好應(yīng)用場景,像之前說的這個ABA問題,數(shù)據(jù)庫層面我們可以加版本號(版本號累加)就能解決,程序中原子類也給我們提供了解決方案:AtomicStampedReference,感興趣的小伙伴可以研究一下。其實思路和版本號類似,比較的時候不僅需要比較期望值,還要對比版本號,都相同的情況下才會做修改。
總結(jié)
以上是生活随笔為你收集整理的shiro如何保证session不失效_请问在不加锁的情况下如何保证线程安全?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 电脑手机汽车全都有 CES上的最佳展品我
- 下一篇: 火爆!国产大飞机C919累计获得32家客
