• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • 多線程6一CAS與自旋鎖

    標簽: Java多線程之美  java  多線程  后端

    1、什么是CAS

    CAS 即 compare and swap 比較并交換, 涉及到三個參數,內存值V, 預期值A, 要更新為的值B, 拿著預期值A與內存值V比較,相等則符合預期,將內存值V更新為B, 不相等,則不能更新V。

    為什么預期值A與內存值V不一樣了呢?

    在多線程環境下,對于臨界區的共享資源,所有線程都可以訪問修改,這時為了保證數據不會發生錯誤,通常會對訪問臨界區資源加鎖,同一時刻最多只能讓一個線程訪問(獨占模式下),這樣會讓線程到臨界區時串行執行,加鎖操作可能會導致并發性能降低,而循環CAS可以實現讓多個線程不加鎖去訪問共享資源,卻也可以保證數據正確性。 如 int share = 1,線程A獲取到share的值1,想要將其修改為2,這時線程B搶先修改share = 3了,線程A這時拿著share =1 預期值與實際內存中已經變為3的值比較, 不相等,cas失敗,這時就重新獲取最新的share再次更新,需要不斷循環,直到更新成功;這里可能會存在線程一直在進行循環cas,消耗cpu資源。

    cas缺點:

    1、存在ABA問題

    2、循環cas, 可能會花費大量時間在循環,浪費cpu資源

    3、只能更新一個值(也可解決,AtomicReference 原子引用類泛型可指定對象,實現一個對象中包含多個屬性值來解決只能更新一個值的問題)

    2、原子類 Atomic

    原子類在JUC的atomic包下提供了 AtomicInteger,AtomicBoolean, AtomicLong等基本數據類型原子類,還有可傳泛型的AtomicReference, 以及帶有版本號的 AtomicStampedReference , 可實現對象的原子更新, 其具體是怎樣保證在多線程環境下,不加鎖的情況也可以原子操作, 是其內部借助了Unsafe類,來保證更新的原子性。

    類圖結構如下:在這里插入圖片描述
    分別用AtomicInteger和 Integer 演示多個線程執行自增操作,是否能夠保證原子性,執行結果是否正確

    代碼如下:

    /**
     * @author zdd
     * 2019/12/22 10:47 上午
     * Description: 演示AtomicInteger原子類原子操作
     */
    public class CasAtomicIntegerTest {
        static  final Integer THREAD_NUMBER = 10;
        static  AtomicInteger atomicInteger = new AtomicInteger(0);
        static  volatile Integer integer = 0;
    
        public static void main(String[] args) throws InterruptedException {
            ThreadTask task = new ThreadTask();
            Thread[] threads = new Thread[THREAD_NUMBER];
            //1,開啟10個線程
            for (int j = 0; j < THREAD_NUMBER; j++) {
                Thread thread  = new Thread(task);
                threads[j]= thread;
            }
            for (Thread thread:threads) {
                //開啟線程
                thread.start();
                //注: join 為了保證主線程在所有子線程執行完畢后再打印結果,否則主線程就阻塞等待
               // thread.join();
            }
    
            // 主線程休眠5s, 等待所有子線程執行完畢再打印
            TimeUnit.SECONDS.sleep(5);
    
            System.out.println("執行完畢,atomicInteger的值為: "+ atomicInteger.get());
            System.out.println("執行完畢,integer的值為 : "+ integer);
        }
    
        public static void  safeIncr() {
            atomicInteger.incrementAndGet();
        }
        public static void  unSafeIncr() {
            integer ++;
        }
    
        static class ThreadTask implements  Runnable{
            @Override
            public void run() {
                // 任務體,分別安全和非安全方式自增1000次
                for (int i = 0; i < 1000; i++) {
                    safeIncr();
                }
                for (int i = 0; i < 1000; i++) {
                    unSafeIncr();
                }
            }
        }
    }
    

    執行結果如下:
    在這里插入圖片描述

    疑問: 上文代碼中注,我本想讓主線程調用每個子線程 join方法,保證主線程在所有子線程執行完畢之后再執行打印結果,然而這樣執行導致非安全的Integer自增結果也正確,猜想是在執行join方法,導致這10個子線程排隊有序在執行了? 因此注釋了該行代碼 ,改為讓主線程休眠幾秒來保證在子線程執行后再打印。

    AtomicInteger如何保證原子性,AtomicInteger持有Unsafe對象,其大部分方法是本地方法,底層實現可保證原子操作。

    public class AtomicInteger extends Number implements java.io.Serializable {
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();
    

    來看一下 AtomicInteger 的自增方法 incrementAndGet(),先自增,再返回增加后的值。

    代碼如下:

      public final int incrementAndGet() {
           //調用unsafe的方法
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
        }
    

    繼續看unsafe如何實現

      public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
              //1.獲取當前對象的內存中的值A
                var5 = this.getIntVolatile(var1, var2);
              //2. var1,var2聯合獲取內存中的值V,var5是期望中的值A, var5+var4 是將要更新為的新值
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
           //3. 更新成功,跳出while循環,返回更新成功時內存中的值(可能下一刻就被其他線程修改)
            return var5;
        }
    

    執行流程圖如下:
    在這里插入圖片描述Unsafe 的compareAndSwapInt是本地方法,可原子地執行更新操作,更新成功返回true,否則false

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    

    3、CAS的ABA問題

    什么是ABA問題?

    例如 線程A獲取變量atomicInteger =100, 想要將其修改為2019 (此時還未修改), 這時線程B搶先進來將atomicInteger先修改為101,再修改回atomicInteger =100,這時線程A開始去更新atomicInteger的值了,此時預期值和內存值相等,更新成功atomicInteger =2019;但是線程A 并不知道這個值其實已經被人修改過了。

    代碼演示如下:

    /**
     * zdd
     * Description: cas的ABA問題
     */
    public class CasTest1 {
    
       // static AtomicInteger atomicInteger = new AtomicInteger(100);
       /* 這里使用原子引用類,傳入Integer類型,
        * 和AtomicInteger一樣,AtomicReference使用更靈活,泛型可指定任何引用類型。
        * 也可用上面注釋代碼
        */
        static AtomicReference<Integer>  reference = new AtomicReference<>(100);
    
        public static void main(String[] args) {
      
          //1.開啟線程A
            new Thread(()-> {
                Integer expect =  reference.get();
                try {
                    //模擬執行任務,讓線程B搶先修改
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println( "執行3s任務后, 修改值是否成功 "+ reference.compareAndSet(expect,2019)+ "  當前值為: "+ reference.get());
            },"A").start();     
        //2.開啟線程B
            new Thread(()-> {
                // expect1 =100
                Integer expect1 =  reference.get();
                //1,先修改為101,再修改回100,產生ABA問題
                reference.compareAndSet(expect1,101);
                //expect2 =101
                Integer expect2 =  reference.get();
                reference.compareAndSet(expect2, 100);
            },"B").start();
    
        }
    }      
    

    執行結果如下:可見線程A修改成功

    A 執行3s任務后, 修改值是否成功:true  當前值為: 2019
    

    4、ABA問題的解決方式

    解決CAS的ABA問題,是參照數據庫樂觀鎖,添加一個版本號,每更新一次,次數+1,就可解決ABA問題了。

    AtomicStampedReference

    /**
     * zdd
     * 2019/11/4 6:30 下午
     * Description:
     */
    public class CasTest1 {
      //設置初始值和版本號
        static  AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    
        public static void main(String[] args) {
            //2,采用帶有版本號的 
            new Thread(()-> {
                Integer  expect = stampedReference.getReference();
                int     stamp = stampedReference.getStamp();
                try {
                    //休眠3s,讓線程B執行完ABA操作
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //此時 stamp=1,與實際版本號3不等,這里更新失敗就是stamp沒有獲取到最新的
                System.out.println("是否修改成功: "+stampedReference.compareAndSet(expect, 101, stamp, stamp +1));
                System.out.println("當前 stamp 值: " + stampedReference.getStamp()+ "當前 reference: " +stampedReference.getReference());
    
            },"A").start();
    
            new Thread(()-> {
                Integer expect = stampedReference.getReference();
                int stamp = stampedReference.getStamp();
                try {
                    //休眠1s,讓線程A獲取都舊的值和版本號
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 1,100 -> 101, 版本號 1-> 2
                stampedReference.compareAndSet(expect, 101 , stamp, stamp+1);
                //2, 101 ->100, 版本號 2->3
                Integer expect2 = stampedReference.getReference();
                stampedReference.compareAndSet(expect2, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
    
            },"B").start();
        }
    }
    

    執行結果如下:

    是否修改成功: false
    當前 stamp 值: 3  當前 reference: 100
    

    5、利用cas實現自旋鎖

    package cas;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * @author zdd
     * 2019/12/22 9:12 下午
     * Description: 利用cas手動實現自旋鎖
     */
    public class SpinLockTest {
    
        static   AtomicReference<Thread>  atomicReference = new AtomicReference<>();
    
        public static void main(String[] args) {
            SpinLockTest spinLockTest = new SpinLockTest();
            //測試使用自旋鎖,達到同步鎖一樣的效果 ,開啟2個子線程
            new Thread(()-> {
                spinLockTest.lock();
                System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis());
                try {
                    //休眠3s
                    TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis());
                spinLockTest.unLock();
            },"線程A").start();
    
            new Thread(()-> {
                spinLockTest.lock();
                System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis());
                try {
                    //休眠3s
                    TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis());
                spinLockTest.unLock();
            },"線程B").start();
        }
        public static void lock() {
          Thread currentThread =  Thread.currentThread();
          for (;;) {
              boolean flag =atomicReference.compareAndSet(null,currentThread);
             //cas更新成功,則跳出循環,否則一直輪詢
              if(flag) {
                  break;
              }
          }
        }
        public static void unLock() {
            Thread currentThread = Thread.currentThread();
            Thread momeryThread  = atomicReference.get();
            //比較內存中線程對象與當前對象,不等拋出異常,防止未獲取到鎖的線程調用unlock
            if(currentThread != momeryThread) {
                throw new IllegalMonitorStateException();
            }
            //釋放鎖
            atomicReference.compareAndSet(currentThread,null);
        }
    }
    

    執行結果如下圖:
    在這里插入圖片描述

    6、總結

    通過全文,我們可以知道cas的概念,它的優缺點;原子類的使用,內部借助Unsafe類循環cas更新操作實現無鎖情況下保證原子更新操作,進一步我們能夠自己利用循環cas實現自旋鎖SpinLock,它與同步鎖如ReentrantLock等區別在于自旋鎖是在未獲取到鎖情況,一直在輪詢,線程時非阻塞的,對cpu資源占用大,適合查詢多修改少場景,并發性能高;同步鎖是未獲取到鎖,阻塞等待,兩者各有適用場景。


    道阻且長,且歌且行!

    每天一小步,踏踏實實走好腳下的路,文章為自己學習總結,不復制黏貼,就是想讓自己的知識沉淀一下,也希望與更多的人交流,如有錯誤,請批評指正!

    版權聲明:本文為qq_39494996原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/qq_39494996/article/details/103658362

    智能推薦

    多線程與高并發(三):基于CAS實現的各種鎖

    共享數據遞增的場景實現: 1,synchronized同步的方式 2,原子類的方式 3,LongAdder的方式 那么問題是,他們的效率都是怎么樣的呢的? 經過一些代碼的測試發現同步方式<原子類操作<LongAdder操作 LongAdder的實現:內部是使用了分段鎖的概念: 其實就是先將數據存到一個數組中,將所有的線程使用分而治之的思想進行同時操作,最后將結果加一下. Reentra...

    HTML中常用操作關于:頁面跳轉,空格

    1.頁面跳轉 2.空格的代替符...

    freemarker + ItextRender 根據模板生成PDF文件

    1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...

    電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!

    Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...

    requests實現全自動PPT模板

    http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...

    猜你喜歡

    Linux C系統編程-線程互斥鎖(四)

    互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...

    統計學習方法 - 樸素貝葉斯

    引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...

    styled-components —— React 中的 CSS 最佳實踐

    https://zhuanlan.zhihu.com/p/29344146 Styled-components 是目前 React 樣式方案中最受關注的一種,它既具備了 css-in-js 的模塊化與參數化優點,又完全使用CSS的書寫習慣,不會引起額外的學習成本。本文是 styled-components 作者之一 Max Stoiber 所寫,首先總結了前端組件化樣式中的最佳實踐原則,然后在此基...

    基于TCP/IP的網絡聊天室用Java來實現

    基于TCP/IP的網絡聊天室實現 開發工具:eclipse 開發環境:jdk1.8 發送端 接收端 工具類 運行截圖...

    精品国产乱码久久久久久蜜桃不卡