本次內(nèi)容主要講原子操作的概念、原子操作的實(shí)現(xiàn)方式、CAS的使用、原理、3大問題及其解決方案,最后還講到了JDK中經(jīng)常使用到的原子操作類。

          ?

          1、什么是原子操作?


          ?  所謂原子操作是指不會(huì)被線程調(diào)度機(jī)制打斷的操作,這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何線程上下文切換。原子操作可以是一個(gè)步驟,也可以是多個(gè)操作步驟,但是其順序不可以被打亂,也不可以被切割而只執(zhí)行其中的一部分。我們常用的i++看起來(lái)雖然簡(jiǎn)單,但這并不是一個(gè)原子操作,具體原理后面單獨(dú)介紹。
          假定有兩個(gè)操作A和B,如果從執(zhí)行A的線程來(lái)看,當(dāng)另一個(gè)線程執(zhí)行B時(shí),要么將B全部執(zhí)行完,要么完全不執(zhí)行B,那么A和B對(duì)彼此來(lái)說(shuō)是原子的。
          將整個(gè)操作視作一個(gè)整體是原子性的核心特征。

          2、如何實(shí)現(xiàn)原子操作?

          2.1 鎖機(jī)制實(shí)現(xiàn)原子操作及其問題

          實(shí)現(xiàn)原子操作可以使用鎖。鎖機(jī)制滿足基本的需求是沒有問題的,但是有的時(shí)候我們的需求并非這么簡(jiǎn)單,我們需要更有效,更加靈活的機(jī)制。synchronized
          關(guān)鍵字是基于阻塞的鎖機(jī)制,也就是說(shuō)當(dāng)一個(gè)線程擁有鎖的時(shí)候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖。使用synchronized關(guān)鍵字存在這樣的問題:

          (1)如果被阻塞的線程優(yōu)先級(jí)很高很重要怎么辦?

          (2)如果獲得鎖的線程一直不釋放鎖怎么辦?

          (3)如果有大量的線程來(lái)競(jìng)爭(zhēng)資源,那CPU將會(huì)花費(fèi)大量的時(shí)間和資源來(lái)處理這些競(jìng)爭(zhēng),同時(shí),還有可能出現(xiàn)一些例如死鎖之類的情況。

          使用鎖機(jī)制是一種比較粗糙、粒度比較大的機(jī)制,我們可以想象多個(gè)線程操作同一個(gè)計(jì)數(shù)器的業(yè)務(wù)場(chǎng)景,使用鎖機(jī)制的話顯得太過笨重。

          2.2 CAS機(jī)制

            實(shí)現(xiàn)原子操作還可以使用當(dāng)前的處理器基本都支持CAS(Compare And Swap)
          的指令,CPU指令集上提供了CAS操作相關(guān)指令,實(shí)現(xiàn)原子操作可以使用這些指令。每一個(gè)CAS操作過程都包含3個(gè)運(yùn)算參數(shù):一個(gè)內(nèi)存地址V,一個(gè)期望的值A(chǔ)和一個(gè)新值
          B,操作的時(shí)候如果這個(gè)地址上存放的值等于這個(gè)期望的值A(chǔ),則將地址上的值賦為新值B,否則不做任何操作。

          2.3 CAS使用

            先來(lái)模擬一個(gè)多個(gè)線程操作同一個(gè)計(jì)數(shù)器的場(chǎng)景,JDK中提供了boolean、int和long基本類型對(duì)應(yīng)的原子包裝類AtomicBoolean、
          AtomicInteger和AtomicLong。我們用AtomicInteger演示,通過
          CountDownLatch進(jìn)行并發(fā)模擬,如果對(duì)CountDownLatch用法不了解,歡迎查看上一篇文章,有通俗易懂的例子。先對(duì)AtomicInteger的主要API做一個(gè)介紹:

          (1)int addAndGet(int delta):以原子方式將輸入的數(shù)值與實(shí)例中的值(AtomicInteger里的value)相加,并返回結(jié)果。

          (2)boolean compareAndSet(int expect,int
          update):如果當(dāng)前數(shù)值等于expect,則以原子方式將當(dāng)前值設(shè)置為update。

          (3)int getAndIncrement():以原子方式將當(dāng)前值加1,注意,這里返回的是自增前的值。

          (4)int getAndSet(int newValue):以原子方式設(shè)置為newValue的值,并返回舊值。
          import java.util.concurrent.CountDownLatch; import
          java.util.concurrent.atomic.AtomicInteger;public class AtomicIntegerDemo {
          static AtomicInteger counter = new AtomicInteger(0); static CountDownLatch
          countDownLatch =new CountDownLatch(20); static class CounterThread implements
          Runnable { @Overridepublic void run() { try { countDownLatch.await(); } catch
          (InterruptedException e) { e.printStackTrace(); } counter.getAndIncrement(); } }
          public static void main(String[] args) throws InterruptedException { for (int i
          = 0; i < 20; i++) { Runnable thread = new CounterThread(); new
          Thread(thread).start(); countDownLatch.countDown(); } Thread.sleep(2000); //
          保證子線程全部執(zhí)行完成 System.out.println("20個(gè)線程并發(fā)執(zhí)行g(shù)etAndIncrement()方法后的結(jié)果:" +
          counter.get()); counter.compareAndSet(20, 18);//如果counter當(dāng)前數(shù)值為20,則以原子方式更新為18
          System.out.println("compareAndSet(20, 18)后的結(jié)果:" + counter.get()); } }
          程序中模擬了20個(gè)線程并發(fā)對(duì)一個(gè)計(jì)數(shù)器進(jìn)行自增操作,結(jié)果輸出為20,可以看到這段代碼并沒有用任何的鎖,也達(dá)到了原子操作目的。



          2.4 CAS原理

            CAS的基本思路就是,如果內(nèi)存地址V上的值和期望的值A(chǔ)相等,則給其賦予新值B,否則不做任何事兒。CAS就是在一個(gè)循環(huán)里不斷的做CAS操作,直到成功為止。
          CAS是怎么實(shí)現(xiàn)線程的安全呢?語(yǔ)言層面不做處理,JDK 調(diào)用這些指令來(lái)完成CAS操作,本質(zhì)上就是將其交給CPU和內(nèi)存,利用CPU
          的多處理能力,實(shí)現(xiàn)硬件層面的阻塞,再加上volatile變量的特性即可實(shí)現(xiàn)基于原子操作的線程安全。用一張圖來(lái)說(shuō)明。




          3、CAS實(shí)現(xiàn)原子操作的三大問題

          ?3.1 ABA問題


            因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個(gè)值原來(lái)是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實(shí)際上卻變化了。舉個(gè)通俗易懂的例子,我的同事老王今年35歲了,還沒有女朋友,我問他有什么要求,給他介紹一個(gè)女朋友。老王就說(shuō)了,只要是沒有結(jié)婚、35歲以下的女的就行。于是我就給他介紹了一個(gè)28歲,剛剛離婚不久的女同志,他還感謝了我好久,可能是他現(xiàn)在都還不知道他這個(gè)女朋友離過婚。這就是典型的ABA問題,只關(guān)心當(dāng)前狀態(tài),而不管中間經(jīng)歷了什么。ABA問題的解決思路就是使用版本號(hào)。給變量追加一個(gè)版本號(hào),每次變量更新的時(shí)候把版本號(hào)加1,那么A→B→A就會(huì)變成1A→2B→3A。就好比老王的要求改成:35歲以下,沒有結(jié)婚并且離婚次數(shù)為0的女性,就不會(huì)發(fā)生剛剛的事情了。


          ?3.2?循環(huán)時(shí)間長(zhǎng)開銷大。

          ?  CAS自旋如果長(zhǎng)時(shí)間不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開銷。

          3.3?只能保證一個(gè)共享變量的原子操作

            
          當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來(lái)保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無(wú)法保證操作的原子性,這個(gè)時(shí)候就可以用鎖。怎么解決這個(gè)問題呢?
          從Java 1.5開始,JDK提供了AtomicReference類來(lái)保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。

          4、JDK中相關(guān)原子操作類

          4.1?AtomicReference

           
           AtomicReference,可以原子更新的對(duì)象引用。AtomicReference有一個(gè)compareAndSet()方法,它可以將已持有引用與預(yù)期引用進(jìn)行比較,如果它們相等,則在AtomicReference對(duì)象內(nèi)設(shè)置一個(gè)新的引用。看一段代碼:
          import java.util.concurrent.atomic.AtomicReference; public class
          AtomicReferenceDemo {static AtomicReference<UserInfo> atomicReference; public
          static void main(String[] args) { //原引用 UserInfo oldUser = new UserInfo("老王", 35
          ); atomicReference= new AtomicReference<>(oldUser); //新引用 UserInfo updateUser =
          new UserInfo("小宋", 21); atomicReference.compareAndSet(oldUser, updateUser);
          System.out.println("使用compareAndSet()替換原有引用后的結(jié)果:" + atomicReference.get());
          System.out.println("原引用:" + oldUser); } static class UserInfo { private String
          name;private int age; public UserInfo(String name, int age) { this.name = name;
          this.age = age; } public String getName() { return name; } public int getAge() {
          return age; } public void setName(String name) { this.name = name; } public void
          setAge(int age) { this.age = age; } @Override public String toString() { return
          "UserInfo{" + "name='" + name + '\'' + ", age=" + age + '}'; } } }
          從程序輸出可以看到,atomicReference的持有的引用被修改了,但是原引用對(duì)象并沒有發(fā)生改變。

          ?

          4.2?AtomicStampedReference

          ?  AtomicStampedReference,利用版本戳的形式記錄了每次改變以后的版本號(hào),這樣的話就不會(huì)存在ABA問題了。
          AtomicStampedReference有一個(gè)內(nèi)部類Pair,使用Pair的int stamp作為計(jì)數(shù)器使用,看下Pair的源碼:
          private static class Pair<T> { final T reference; final int stamp; private
          Pair(T reference,int stamp) { this.reference = reference; this.stamp = stamp; }
          static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference,
          stamp); } }
          還是老王那個(gè)例子,如果使用AtomicStampedReference的話,老王更關(guān)心的是介紹的女朋友離過幾次婚。用一段代碼來(lái)模擬給老王介紹女朋友的場(chǎng)景:
          import java.util.concurrent.atomic.AtomicStampedReference; public class
          AtomicStampedReferenceDemo {static AtomicStampedReference<String> asr = new
          AtomicStampedReference("介紹的女朋友", 0); public static void main(String[] args)
          throws InterruptedException { final String oldReference = asr.getReference();//
          初始值,表示介紹的女朋友 final int oldStamp = asr.getStamp();//初始版本0,表示介紹的女朋友沒有離過婚 Thread
          thread1= new Thread(() -> { String newReference = oldReference + " 離婚1次";
          boolean first = asr.compareAndSet(oldReference, newReference, oldStamp, oldStamp
          + 1); if (first) { System.out.println("介紹的女朋友第一次離婚。。。"); } boolean second =
          asr.compareAndSet(newReference, oldReference + "又離婚了", oldStamp + 1, oldStamp +
          2); if (second) { System.out.println("介紹的女朋友第二次離婚。。。"); } }, "介紹的女朋友離婚");
          Thread thread2= new Thread(() -> { String reference = asr.getReference();//
          介紹的女朋友最新狀態(tài)//判斷介紹的女朋友最新狀態(tài)是否符合老王的要求 boolean flag = asr.compareAndSet(reference,
          reference + "沒有離過婚", oldStamp, oldStamp + 1); if (flag) { System.out.println(
          "老王笑嘻嘻地對(duì)我說(shuō),介紹的女朋友符合我的要求"); } else { System.out.println("老王拳頭緊握地對(duì)我說(shuō),介紹的女朋友居然離過"
          + asr.getStamp() + "次婚,不符合我要求!?。?!"); } }, "老王相親"); thread1.start();
          thread1.join(); thread2.start(); thread2.join(); } }
          ?啟動(dòng)2個(gè)子線程,分別代表介紹的女朋友多次離婚以及老王相親的場(chǎng)景。從程序輸出可以看到,介紹的女朋友不符合老王的要求,老王為了避免喜當(dāng)?shù)?,果斷拒絕了。



          ?老王判斷的依據(jù)是,介紹的女朋友應(yīng)該是沒有離過婚,stamp值等于0才對(duì)。但是老王仔細(xì)一看,stamp已經(jīng)是2,不符合我的要求,不能要。



          4.3?AtomicMarkableReference

          ?  
          AtomicMarkableReference,可以原子更新一個(gè)布爾類型的標(biāo)記位和引用類型。構(gòu)造方法是AtomicMarkableReference(V
          initialRef,booleaninitialMark)。AtomicMarkableReference也有一個(gè)內(nèi)部類Pair,使用Pair的boolean
          ?
          mark來(lái)標(biāo)記狀態(tài)。還是老王那個(gè)例子,使用AtomicStampedReference可能關(guān)心的是離婚次數(shù),AtomicMarkableReference關(guān)心的是有沒有離過婚。用一段代碼來(lái)模擬:
          import java.util.concurrent.atomic.AtomicMarkableReference; public class
          AtomicMarkableReferenceDemo {static AtomicMarkableReference markableReference;
          public static void main(String[] args) throws InterruptedException { String girl
          = "介紹的女朋友"; markableReference = new AtomicMarkableReference(girl, false);
          Thread t1= new Thread(() -> { markableReference.compareAndSet(girl, girl + "離婚",
          false, true); System.out.println(markableReference.getReference()); },
          "介紹的女朋友離婚了"); Thread t2 = new Thread(() -> { //老王檢查標(biāo)記,只關(guān)心這個(gè)標(biāo)志位 boolean marked =
          markableReference.isMarked();if (marked) { System.out.println(
          "你給我介紹的女朋友離過婚,我不要!!"); } else { System.out.println("兄弟,大兄弟,親生兄弟啊??!這個(gè)女朋友我要了"); }
          },"老王鑒定介紹的女朋友有沒有離過婚"); t1.start(); t1.join(); t2.start(); t2.join(); } }
          程序輸出可以看到,老王還是堅(jiān)持了自己的原則。



          4.4?AtomicIntegerArray

          ?  AtomicIntegerArray,元素可以原子更新的數(shù)組。其常用方法如下:

          (1)int addAndGet(int i,int delta):以原子方式將輸入值與數(shù)組中索引i的元素相加。

          (2)boolean compareAndSet(int i,int expect,int
          update):如果當(dāng)前值等于預(yù)期值,則以原子方式將數(shù)組位置i的元素設(shè)置成update值。


          需要注意的是,數(shù)組value通過構(gòu)造方法傳遞進(jìn)去,然后AtomicIntegerArray會(huì)將當(dāng)前數(shù)組復(fù)制一份,所以當(dāng)AtomicIntegerArray對(duì)內(nèi)部的數(shù)組元素進(jìn)行修改時(shí),不會(huì)影響傳入的數(shù)組。

          用法比較簡(jiǎn)單,看一個(gè)例子:
          public class AtomicIntegerArrayDemo { static int[] value = new int[]{1, 2};//
          原始數(shù)組 static AtomicIntegerArray atomicIntegerArray = new
          AtomicIntegerArray(value);public static void main(String[] args) {
          atomicIntegerArray.getAndSet(0, 3); System.out.println(
          "atomicIntegerArray的第一個(gè)元素:" + atomicIntegerArray.get(0)); System.out.println(
          "原始數(shù)組的第一個(gè)元素:" + value[0]);//原數(shù)組不會(huì)變化 } }
          程序輸出可以看到,原始數(shù)組并沒有受到影響。



          ?順便看一下AtomicIntegerArray的構(gòu)造方法:
          public AtomicIntegerArray(int[] array) { // Visibility guaranteed by final
          field guarantees this.array = array.clone(); }
          ?5、結(jié)語(yǔ)

            文中例子純屬虛構(gòu),便于對(duì)知識(shí)點(diǎn)的理解,不摻雜任何其他意思。下一篇內(nèi)容中會(huì)介紹Java的顯示鎖Lock相關(guān)知識(shí)點(diǎn),閱讀過程中如發(fā)現(xiàn)描述有誤,請(qǐng)指出,謝謝。


          ?

          ?

          友情鏈接
          ioDraw流程圖
          API參考文檔
          OK工具箱
          云服務(wù)器優(yōu)惠
          阿里云優(yōu)惠券
          騰訊云優(yōu)惠券
          京東云優(yōu)惠券
          站點(diǎn)信息
          問題反饋
          郵箱:[email protected]
          QQ群:637538335
          關(guān)注微信

                思思99热 | 欧美精品久久久久久久免费软件 | 三上悠亚中文字幕在线播放 | 精品无码一区二区三区爱与 | 自拍偷拍成人网站 |