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