溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java怎么實現(xiàn)手寫自旋鎖

發(fā)布時間:2022-08-18 09:36:49 來源:億速云 閱讀:111 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“Java怎么實現(xiàn)手寫自旋鎖”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

    前言

    我們在寫并發(fā)程序的時候,一個非常常見的需求就是保證在某一個時刻只有一個線程執(zhí)行某段代碼,像這種代碼叫做臨界區(qū),而通常保證一個時刻只有一個線程執(zhí)行臨界區(qū)的代碼的方法就是鎖。在本篇文章當中我們將會仔細分析和學習自旋鎖,所謂自旋鎖就是通過while循環(huán)實現(xiàn)的,讓拿到鎖的線程進入臨界區(qū)執(zhí)行代碼,讓沒有拿到鎖的線程一直進行while死循環(huán),這其實就是線程自己“旋”在while循環(huán)了,因而這種鎖就叫做自旋鎖。

    自旋鎖

    原子性

    在談自旋鎖之前就不得不談原子性了。所謂原子性簡單說來就是一個一個操作要么不做要么全做,全做的意思就是在操作的過程當中不能夠被中斷,比如說對變量data進行加一操作,有以下三個步驟:

    • 將data從內(nèi)存加載到寄存器。

    • 將data這個值加一。

    • 將得到的結(jié)果寫回內(nèi)存。

    原子性就表示一個線程在進行加一操作的時候,不能夠被其他線程中斷,只有這個線程執(zhí)行完這三個過程的時候其他線程才能夠操作數(shù)據(jù)data。

    我們現(xiàn)在用代碼體驗一下,在Java當中我們可以使用AtomicInteger進行對整型數(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); // 對數(shù)據(jù) data 進行原子加1操作
          }
        });
        Thread t2 = new Thread(() -> {
          for (int i = 0; i < 100000; i++) {
            data.addAndGet(1);// 對數(shù)據(jù) data 進行原子加1操作
          }
        });
        // 啟動兩個線程
        t1.start();
        t2.start();
        // 等待兩個線程執(zhí)行完成
        t1.join();
        t2.join();
        // 打印最終的結(jié)果
        System.out.println(data); // 200000
      }
    }

    從上面的代碼分析可以知道,如果是一般的整型變量如果兩個線程同時進行操作的時候,最終的結(jié)果是會小于200000。

    我們現(xiàn)在來模擬一下一般的整型變量出現(xiàn)問題的過程:

    主內(nèi)存data的初始值等于0,兩個線程得到的data初始值都等于0。

    Java怎么實現(xiàn)手寫自旋鎖

    現(xiàn)在線程一將data加一,然后線程一將data的值同步回主內(nèi)存,整個內(nèi)存的數(shù)據(jù)變化如下:

    Java怎么實現(xiàn)手寫自旋鎖

    現(xiàn)在線程二data加一,然后將data的值同步回主內(nèi)存(將原來主內(nèi)存的值覆蓋掉了):

    Java怎么實現(xiàn)手寫自旋鎖

    我們本來希望data的值在經(jīng)過上面的變化之后變成2,但是線程二覆蓋了我們的值,因此在多線程情況下,會使得我們最終的結(jié)果變小。

    但是在上面的程序當中我們最終的輸出結(jié)果是等于20000的,這是因為給data進行+1的操作是原子的不可分的,在操作的過程當中其他線程是不能對data進行操作的。這就是原子性帶來的優(yōu)勢。

    自己動手寫自旋鎖

    AtomicInteger類

    現(xiàn)在我們已經(jīng)了解了原子性的作用了,我們現(xiàn)在來了解AtomicInteger類的另外一個原子性的操作&mdash;&mdash;compareAndSet,這個操作叫做比較并交換(CAS),他具有原子性。

    public static void main(String[] args) {
      AtomicInteger atomicInteger = new AtomicInteger();
      atomicInteger.set(0);
      atomicInteger.compareAndSet(0, 1);
    }

    compareAndSet函數(shù)的意義:首先會比較第一個參數(shù)(對應上面的代碼就是0)和atomicInteger的值,如果相等則進行交換,也就是將atomicInteger的值設置為第二個參數(shù)(對應上面的代碼就是1),如果這些操作成功,那么compareAndSet函數(shù)就返回true,如果操作失敗則返回false,操作失敗可能是因為第一個參數(shù)的值(期望值)和atomicInteger不相等,如果相等也可能因為在更改atomicInteger的值的時候失?。ㄒ驗榭赡苡卸鄠€線程在操作,因為原子性的存在,只能有一個線程操作成功)。

    自旋鎖實現(xiàn)原理

    我們可以使用AtomicInteger類實現(xiàn)自旋鎖,我們可以用0這個值表示未上鎖,1這個值表示已經(jīng)上鎖了。

    AtomicInteger類的初始值為0。

    在上鎖時,我們可以使用代碼atomicInteger.compareAndSet(0, 1)進行實現(xiàn),我們在前面已經(jīng)提到了只能夠有一個線程完成這個操作,也就是說只能有一個線程調(diào)用這行代碼然后返回true其余線程都返回false,這些返回false的線程不能夠進入臨界區(qū),因此我們需要這些線程停在atomicInteger.compareAndSet(0, 1)這行代碼不能夠往下執(zhí)行,我們可以使用while循環(huán)讓這些線程一直停在這里while (!value.compareAndSet(0, 1));,只有返回true的線程才能夠跳出循環(huán),其余線程都會一直在這里循環(huán),我們稱這種行為叫做自旋,這種鎖因而也被叫做自旋鎖。

    線程在出臨界區(qū)的時候需要重新將鎖的狀態(tài)調(diào)整為未上鎖的上狀態(tài),我們使用代碼value.compareAndSet(1, 0);就可以實現(xiàn),將鎖的狀態(tài)還原為未上鎖的狀態(tài),這樣其他的自旋的線程就可以拿到鎖,然后進入臨界區(qū)了。

    自旋鎖代碼實現(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();
        // 設置 value 的初始值為0 表示未上鎖的狀態(tài)
        this.value.set(0);
      }
     
      public void lock() {
        // 進行自旋操作
        while (!value.compareAndSet(0, 1));
      }
     
      public void unlock() {
        // 將鎖的狀態(tài)設置為未上鎖狀態(tài)
        value.compareAndSet(1, 0);
      }
     
    }

    上面就是我們自己實現(xiàn)的自旋鎖的代碼,這看起來實在太簡單了,但是它確實幫助我們實現(xiàn)了一個鎖,而且能夠在真實場景進行使用的,我們現(xiàn)在用代碼對上面我們寫的鎖進行測試。

    測試程序:

    public class SpinLockTest {
     
      public static int data;
      public static SpinLock lock = new SpinLock();
     
      public static void add() {
        for (int i = 0; i < 100000; i++) {
          // 上鎖 只能有一個線程執(zhí)行 data++ 操作 其余線程都只能進行while循環(huán)
          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個線程執(zhí)行完成
        for (int i = 0; i < 100; i++) {
          threads[i].join();
        }
        System.out.println(data); // 10000000
      }
    }

    在上面的代碼單中,我們使用100個線程,然后每個線程循環(huán)執(zhí)行100000data++操作,上面的代碼最后輸出的結(jié)果是10000000,和我們期待的結(jié)果是相等的,這就說明我們實現(xiàn)的自旋鎖是正確的。

    自己動手寫可重入自旋鎖

    可重入自旋鎖

    在上面實現(xiàn)的自旋鎖當中已經(jīng)可以滿足一些我們的基本需求了,就是一個時刻只能夠有一個線程執(zhí)行臨界區(qū)的代碼。但是上面的的代碼并不能夠滿足重入的需求,也就是說上面寫的自旋鎖并不是一個可重入的自旋鎖,事實上在上面實現(xiàn)的自旋鎖當中重入的話就會產(chǎn)生死鎖。

    我們通過一份代碼來模擬上面重入產(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進入臨界區(qū) state = " + state);
        for (int i = 0; i < 10; i++)
          data++;
        add(state + 1); // 進行遞歸重入 重入之前鎖狀態(tài)已經(jīng)是1了 因為這個線程進入了臨界區(qū)
        lock.unlock();
      }
    }

    在上面的代碼當中加入我們傳入的參數(shù)state的值為1,那么在線程執(zhí)行for循環(huán)之后再次遞歸調(diào)用add函數(shù)的話,那么state的值就變成了2。

    if條件仍然滿足,這個線程也需要重新獲得鎖,但是此時鎖的狀態(tài)是1,這個線程已經(jīng)獲得過一次鎖了,但是自旋鎖期待的鎖的狀態(tài)是0,因為只有這樣他才能夠再次獲得鎖,進入臨界區(qū),但是現(xiàn)在鎖的狀態(tài)是1,也就是說雖然這個線程獲得過一次鎖,但是它也會一直進行while循環(huán)而且永遠都出不來了,這樣就形成了死鎖了。

    可重入自旋鎖思想

    針對上面這種情況我們需要實現(xiàn)一個可重入的自旋鎖,我們的思想大致如下:

    • 在我們實現(xiàn)的自旋鎖當中,我們可以增加兩個變量,owner一個用于存當前擁有鎖的線程,count一個記錄當前線程進入鎖的次數(shù)。

    • 如果線程獲得鎖,owner = Thread.currentThread()并且count = 1。

    • 當線程下次再想獲取鎖的時候,首先先看owner是不是指向自己,則一直進行循環(huán)操作,如果是則直接進行count++操作,然后就可以進入臨界區(qū)了。

    • 我們在出臨界區(qū)的時候,如果count大于一的話,說明這個線程重入了這把鎖,因此不能夠直接將鎖設置為0也就是未上鎖的狀態(tài),這種情況直接進行count--操作,如果count等于1的話,說明線程當前的狀態(tài)不是重入狀態(tài)(可能是重入之后遞歸返回了),因此在出臨界區(qū)之前需要將鎖的狀態(tài)設置為0,也就是沒上鎖的狀態(tài),好讓其他線程能夠獲取鎖。

    可重入鎖代碼實現(xiàn)

    實現(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--;
      }
    }

    下面我們通過一個遞歸程序去驗證我們寫的可重入的自旋鎖是否能夠成功工作。

    測試程序:

    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進入臨界區(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    進入臨界區(qū) state = 1
    Thread-3    進入臨界區(qū) state = 2
    Thread-3    進入臨界區(qū) state = 3
    Thread-0    進入臨界區(qū) state = 1
    Thread-0    進入臨界區(qū) state = 2
    Thread-0    進入臨界區(qū) state = 3
    Thread-9    進入臨界區(qū) state = 1
    Thread-9    進入臨界區(qū) state = 2
    Thread-9    進入臨界區(qū) state = 3
    Thread-4    進入臨界區(qū) state = 1
    Thread-4    進入臨界區(qū) state = 2
    Thread-4    進入臨界區(qū) state = 3
    Thread-7    進入臨界區(qū) state = 1
    Thread-7    進入臨界區(qū) state = 2
    Thread-7    進入臨界區(qū) state = 3
    Thread-8    進入臨界區(qū) state = 1
    Thread-8    進入臨界區(qū) state = 2
    Thread-8    進入臨界區(qū) state = 3
    Thread-5    進入臨界區(qū) state = 1
    Thread-5    進入臨界區(qū) state = 2
    Thread-5    進入臨界區(qū) state = 3
    Thread-2    進入臨界區(qū) state = 1
    Thread-2    進入臨界區(qū) state = 2
    Thread-2    進入臨界區(qū) state = 3
    Thread-6    進入臨界區(qū) state = 1
    Thread-6    進入臨界區(qū) state = 2
    Thread-6    進入臨界區(qū) state = 3
    Thread-1    進入臨界區(qū) state = 1
    Thread-1    進入臨界區(qū) state = 2
    Thread-1    進入臨界區(qū) state = 3
    300

    從上面的輸出結(jié)果我們就可以知道,當一個線程能夠獲取鎖的時候他能夠進行重入,而且最終輸出的結(jié)果也是正確的,因此驗證了我們寫了可重入自旋鎖是有效的!

    “Java怎么實現(xiàn)手寫自旋鎖”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI