您好,登錄后才能下訂單哦!
這篇文章主要介紹“怎么使用Java實(shí)現(xiàn)手動(dòng)自旋鎖”,在日常操作中,相信很多人在怎么使用Java實(shí)現(xiàn)手動(dòng)自旋鎖問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”怎么使用Java實(shí)現(xiàn)手動(dòng)自旋鎖”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
在談自旋鎖之前就不得不談原子性了。所謂原子性簡(jiǎn)單說(shuō)來(lái)就是一個(gè)一個(gè)操作要么不做要么全做,全做的意思就是在操作的過(guò)程當(dāng)中不能夠被中斷,比如說(shuō)對(duì)變量data進(jìn)行加一操作,有以下三個(gè)步驟:
將data從內(nèi)存加載到寄存器。
將data這個(gè)值加一。
將得到的結(jié)果寫(xiě)回內(nèi)存。
原子性就表示一個(gè)線(xiàn)程在進(jìn)行加一操作的時(shí)候,不能夠被其他線(xiàn)程中斷,只有這個(gè)線(xiàn)程執(zhí)行完這三個(gè)過(guò)程的時(shí)候其他線(xiàn)程才能夠操作數(shù)據(jù)data。
我們現(xiàn)在用代碼體驗(yàn)一下,在Java當(dāng)中我們可以使用AtomicInteger進(jìn)行對(duì)整型數(shù)據(jù)的原子操作:
import java.util.concurrent.atomic.AtomicInteger; public class AtomicDemo { public static void main(String[] args) throws InterruptedException { AtomicInteger data = new AtomicInteger(); data.set(0); // 將數(shù)據(jù)初始化位0 Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1); // 對(duì)數(shù)據(jù) data 進(jìn)行原子加1操作 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1);// 對(duì)數(shù)據(jù) data 進(jìn)行原子加1操作 } }); // 啟動(dòng)兩個(gè)線(xiàn)程 t1.start(); t2.start(); // 等待兩個(gè)線(xiàn)程執(zhí)行完成 t1.join(); t2.join(); // 打印最終的結(jié)果 System.out.println(data); // 200000 } }
從上面的代碼分析可以知道,如果是一般的整型變量如果兩個(gè)線(xiàn)程同時(shí)進(jìn)行操作的時(shí)候,最終的結(jié)果是會(huì)小于200000。
我們現(xiàn)在來(lái)模擬一下一般的整型變量出現(xiàn)問(wèn)題的過(guò)程:
主內(nèi)存data的初始值等于0,兩個(gè)線(xiàn)程得到的data初始值都等于0。
現(xiàn)在線(xiàn)程一將data加一,然后線(xiàn)程一將data的值同步回主內(nèi)存,整個(gè)內(nèi)存的數(shù)據(jù)變化如下:
現(xiàn)在線(xiàn)程二data加一,然后將data的值同步回主內(nèi)存(將原來(lái)主內(nèi)存的值覆蓋掉了):
我們本來(lái)希望data的值在經(jīng)過(guò)上面的變化之后變成2,但是線(xiàn)程二覆蓋了我們的值,因此在多線(xiàn)程情況下,會(huì)使得我們最終的結(jié)果變小。
但是在上面的程序當(dāng)中我們最終的輸出結(jié)果是等于20000的,這是因?yàn)榻odata進(jìn)行+1的操作是原子的不可分的,在操作的過(guò)程當(dāng)中其他線(xiàn)程是不能對(duì)data進(jìn)行操作的。這就是原子性帶來(lái)的優(yōu)勢(shì)。
AtomicInteger類(lèi)
現(xiàn)在我們已經(jīng)了解了原子性的作用了,我們現(xiàn)在來(lái)了解AtomicInteger類(lèi)的另外一個(gè)原子性的操作——compareAndSet,這個(gè)操作叫做比較并交換(CAS),他具有原子性。
public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.set(0); atomicInteger.compareAndSet(0, 1); }
compareAndSet函數(shù)的意義:首先會(huì)比較第一個(gè)參數(shù)(對(duì)應(yīng)上面的代碼就是0)和atomicInteger的值,如果相等則進(jìn)行交換,也就是將atomicInteger的值設(shè)置為第二個(gè)參數(shù)(對(duì)應(yīng)上面的代碼就是1),如果這些操作成功,那么compareAndSet函數(shù)就返回true,如果操作失敗則返回false,操作失敗可能是因?yàn)榈谝粋€(gè)參數(shù)的值(期望值)和atomicInteger不相等,如果相等也可能因?yàn)樵诟腶tomicInteger的值的時(shí)候失?。ㄒ?yàn)榭赡苡卸鄠€(gè)線(xiàn)程在操作,因?yàn)樵有缘拇嬖?,只能有一個(gè)線(xiàn)程操作成功)。
自旋鎖實(shí)現(xiàn)原理
我們可以使用AtomicInteger類(lèi)實(shí)現(xiàn)自旋鎖,我們可以用0這個(gè)值表示未上鎖,1這個(gè)值表示已經(jīng)上鎖了。
AtomicInteger類(lèi)的初始值為0。
在上鎖時(shí),我們可以使用代碼atomicInteger.compareAndSet(0, 1)進(jìn)行實(shí)現(xiàn),我們?cè)谇懊嬉呀?jīng)提到了只能夠有一個(gè)線(xiàn)程完成這個(gè)操作,也就是說(shuō)只能有一個(gè)線(xiàn)程調(diào)用這行代碼然后返回true其余線(xiàn)程都返回false,這些返回false的線(xiàn)程不能夠進(jìn)入臨界區(qū),因此我們需要這些線(xiàn)程停在atomicInteger.compareAndSet(0, 1)這行代碼不能夠往下執(zhí)行,我們可以使用while循環(huán)讓這些線(xiàn)程一直停在這里while (!value.compareAndSet(0, 1));,只有返回true的線(xiàn)程才能夠跳出循環(huán),其余線(xiàn)程都會(huì)一直在這里循環(huán),我們稱(chēng)這種行為叫做自旋,這種鎖因而也被叫做自旋鎖。
線(xiàn)程在出臨界區(qū)的時(shí)候需要重新將鎖的狀態(tài)調(diào)整為未上鎖的上狀態(tài),我們使用代碼value.compareAndSet(1, 0);就可以實(shí)現(xiàn),將鎖的狀態(tài)還原為未上鎖的狀態(tài),這樣其他的自旋的線(xiàn)程就可以拿到鎖,然后進(jìn)入臨界區(qū)了。
自旋鎖代碼實(shí)現(xiàn)
import java.util.concurrent.atomic.AtomicInteger; public class SpinLock { // 0 表示未上鎖狀態(tài) // 1 表示上鎖狀態(tài) protected AtomicInteger value; public SpinLock() { this.value = new AtomicInteger(); // 設(shè)置 value 的初始值為0 表示未上鎖的狀態(tài) this.value.set(0); } public void lock() { // 進(jìn)行自旋操作 while (!value.compareAndSet(0, 1)); } public void unlock() { // 將鎖的狀態(tài)設(shè)置為未上鎖狀態(tài) value.compareAndSet(1, 0); } }
上面就是我們自己實(shí)現(xiàn)的自旋鎖的代碼,這看起來(lái)實(shí)在太簡(jiǎn)單了,但是它確實(shí)幫助我們實(shí)現(xiàn)了一個(gè)鎖,而且能夠在真實(shí)場(chǎng)景進(jìn)行使用的,我們現(xiàn)在用代碼對(duì)上面我們寫(xiě)的鎖進(jìn)行測(cè)試。
測(cè)試程序:
public class SpinLockTest { public static int data; public static SpinLock lock = new SpinLock(); public static void add() { for (int i = 0; i < 100000; i++) { // 上鎖 只能有一個(gè)線(xiàn)程執(zhí)行 data++ 操作 其余線(xiàn)程都只能進(jìn)行while循環(huán) lock.lock(); data++; lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[100]; // 設(shè)置100個(gè)線(xiàn)程 for (int i = 0; i < 100; i ++) { threads[i] = new Thread(SpinLockTest::add); } // 啟動(dòng)一百個(gè)線(xiàn)程 for (int i = 0; i < 100; i++) { threads[i].start(); } // 等待這100個(gè)線(xiàn)程執(zhí)行完成 for (int i = 0; i < 100; i++) { threads[i].join(); } System.out.println(data); // 10000000 } }
在上面的代碼單中,我們使用100個(gè)線(xiàn)程,然后每個(gè)線(xiàn)程循環(huán)執(zhí)行100000data++操作,上面的代碼最后輸出的結(jié)果是10000000,和我們期待的結(jié)果是相等的,這就說(shuō)明我們實(shí)現(xiàn)的自旋鎖是正確的。
可重入自旋鎖
在上面實(shí)現(xiàn)的自旋鎖當(dāng)中已經(jīng)可以滿(mǎn)足一些我們的基本需求了,就是一個(gè)時(shí)刻只能夠有一個(gè)線(xiàn)程執(zhí)行臨界區(qū)的代碼。但是上面的的代碼并不能夠滿(mǎn)足重入的需求,也就是說(shuō)上面寫(xiě)的自旋鎖并不是一個(gè)可重入的自旋鎖,事實(shí)上在上面實(shí)現(xiàn)的自旋鎖當(dāng)中重入的話(huà)就會(huì)產(chǎn)生死鎖。
我們通過(guò)一份代碼來(lái)模擬上面重入產(chǎn)生死鎖的情況:
public static void add(int state) throws InterruptedException { TimeUnit.SECONDS.sleep(1); if (state <= 3) { lock.lock(); System.out.println(Thread.currentThread().getName() + "\t進(jìn)入臨界區(qū) state = " + state); for (int i = 0; i < 10; i++) data++; add(state + 1); // 進(jìn)行遞歸重入 重入之前鎖狀態(tài)已經(jīng)是1了 因?yàn)檫@個(gè)線(xiàn)程進(jìn)入了臨界區(qū) lock.unlock(); } }
在上面的代碼當(dāng)中加入我們傳入的參數(shù)state的值為1,那么在線(xiàn)程執(zhí)行for循環(huán)之后再次遞歸調(diào)用add函數(shù)的話(huà),那么state的值就變成了2。
if條件仍然滿(mǎn)足,這個(gè)線(xiàn)程也需要重新獲得鎖,但是此時(shí)鎖的狀態(tài)是1,這個(gè)線(xiàn)程已經(jīng)獲得過(guò)一次鎖了,但是自旋鎖期待的鎖的狀態(tài)是0,因?yàn)橹挥羞@樣他才能夠再次獲得鎖,進(jìn)入臨界區(qū),但是現(xiàn)在鎖的狀態(tài)是1,也就是說(shuō)雖然這個(gè)線(xiàn)程獲得過(guò)一次鎖,但是它也會(huì)一直進(jìn)行while循環(huán)而且永遠(yuǎn)都出不來(lái)了,這樣就形成了死鎖了。
可重入自旋鎖思想
針對(duì)上面這種情況我們需要實(shí)現(xiàn)一個(gè)可重入的自旋鎖,我們的思想大致如下:
在我們實(shí)現(xiàn)的自旋鎖當(dāng)中,我們可以增加兩個(gè)變量,owner一個(gè)用于存當(dāng)前擁有鎖的線(xiàn)程,count一個(gè)記錄當(dāng)前線(xiàn)程進(jìn)入鎖的次數(shù)。
如果線(xiàn)程獲得鎖,owner = Thread.currentThread()并且count = 1。
當(dāng)線(xiàn)程下次再想獲取鎖的時(shí)候,首先先看owner是不是指向自己,則一直進(jìn)行循環(huán)操作,如果是則直接進(jìn)行count++操作,然后就可以進(jìn)入臨界區(qū)了。
我們?cè)诔雠R界區(qū)的時(shí)候,如果count大于一的話(huà),說(shuō)明這個(gè)線(xiàn)程重入了這把鎖,因此不能夠直接將鎖設(shè)置為0也就是未上鎖的狀態(tài),這種情況直接進(jìn)行count--操作,如果count等于1的話(huà),說(shuō)明線(xiàn)程當(dāng)前的狀態(tài)不是重入狀態(tài)(可能是重入之后遞歸返回了),因此在出臨界區(qū)之前需要將鎖的狀態(tài)設(shè)置為0,也就是沒(méi)上鎖的狀態(tài),好讓其他線(xiàn)程能夠獲取鎖。
可重入鎖代碼實(shí)現(xiàn)
實(shí)現(xiàn)的可重入鎖代碼如下:
public class ReentrantSpinLock extends SpinLock { private Thread owner; private int count; @Override public void lock() { if (owner == null || owner != Thread.currentThread()) { while (!value.compareAndSet(0, 1)); owner = Thread.currentThread(); count = 1; }else { count++; } } @Override public void unlock() { if (count == 1) { count = 0; value.compareAndSet(1, 0); }else count--; } }
下面我們通過(guò)一個(gè)遞歸程序去驗(yàn)證我們寫(xiě)的可重入的自旋鎖是否能夠成功工作。
測(cè)試程序:
import java.util.concurrent.TimeUnit; public class ReentrantSpinLockTest { public static int data; public static ReentrantSpinLock lock = new ReentrantSpinLock(); public static void add(int state) throws InterruptedException { TimeUnit.SECONDS.sleep(1); if (state <= 3) { lock.lock(); System.out.println(Thread.currentThread().getName() + "\t進(jìn)入臨界區(qū) state = " + state); for (int i = 0; i < 10; i++) data++; add(state + 1); lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(new Thread(() -> { try { ReentrantSpinLockTest.add(1); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i))); } for (int i = 0; i < 10; i++) { threads[i].start(); } for (int i = 0; i < 10; i++) { threads[i].join(); } System.out.println(data); } }
上面程序的輸出:
Thread-3 進(jìn)入臨界區(qū) state = 1
Thread-3 進(jìn)入臨界區(qū) state = 2
Thread-3 進(jìn)入臨界區(qū) state = 3
Thread-0 進(jìn)入臨界區(qū) state = 1
Thread-0 進(jìn)入臨界區(qū) state = 2
Thread-0 進(jìn)入臨界區(qū) state = 3
Thread-9 進(jìn)入臨界區(qū) state = 1
Thread-9 進(jìn)入臨界區(qū) state = 2
Thread-9 進(jìn)入臨界區(qū) state = 3
Thread-4 進(jìn)入臨界區(qū) state = 1
Thread-4 進(jìn)入臨界區(qū) state = 2
Thread-4 進(jìn)入臨界區(qū) state = 3
Thread-7 進(jìn)入臨界區(qū) state = 1
Thread-7 進(jìn)入臨界區(qū) state = 2
Thread-7 進(jìn)入臨界區(qū) state = 3
Thread-8 進(jìn)入臨界區(qū) state = 1
Thread-8 進(jìn)入臨界區(qū) state = 2
Thread-8 進(jìn)入臨界區(qū) state = 3
Thread-5 進(jìn)入臨界區(qū) state = 1
Thread-5 進(jìn)入臨界區(qū) state = 2
Thread-5 進(jìn)入臨界區(qū) state = 3
Thread-2 進(jìn)入臨界區(qū) state = 1
Thread-2 進(jìn)入臨界區(qū) state = 2
Thread-2 進(jìn)入臨界區(qū) state = 3
Thread-6 進(jìn)入臨界區(qū) state = 1
Thread-6 進(jìn)入臨界區(qū) state = 2
Thread-6 進(jìn)入臨界區(qū) state = 3
Thread-1 進(jìn)入臨界區(qū) state = 1
Thread-1 進(jìn)入臨界區(qū) state = 2
Thread-1 進(jìn)入臨界區(qū) state = 3
300
從上面的輸出結(jié)果我們就可以知道,當(dāng)一個(gè)線(xiàn)程能夠獲取鎖的時(shí)候他能夠進(jìn)行重入,而且最終輸出的結(jié)果也是正確的,因此驗(yàn)證了我們寫(xiě)了可重入自旋鎖是有效的!
到此,關(guān)于“怎么使用Java實(shí)現(xiàn)手動(dòng)自旋鎖”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。