Java進階專題(十三) 從電商系統角度研究多執行緒(上)

有夢想的老王發表於2020-09-07

前言

​ 本章節主要分享下,多執行緒併發在電商系統下的應用。主要從以下幾個方面深入:執行緒相關的基礎理論和工具、多執行緒程式下的效能調優和電商場景下多執行緒的使用。

多執行緒J·U·C

執行緒池

概念

回顧執行緒建立的方式

  • 繼承Thread
  • 實現Runnable
  • 使用FutureTask

執行緒狀態

NEW:剛剛建立,沒做任何操作

RUNNABLE:呼叫run,可以執行,但不代表一定在執行(RUNNING,READY)

WATING:使用了waite(),join()等方法

TIMED_WATING:使用了sleep(long),wait(long),join(long)等方法

BLOCKED:搶不到鎖

TERMINATED:終止

執行緒池基本概念

​ 根據上面的狀態,普通執行緒執行完,就會進入TERMINA TED銷燬掉,而執行緒池就是建立一個緩衝池存放執行緒,執行結束以後,該執行緒並不會死亡,而是再次返回執行緒池中成為空閒狀態,等候下次任務來臨,這使得執行緒池比手動建立執行緒有著更多的優勢:

  • 降低系統資源消耗,通過重用已存在的執行緒,降低執行緒建立和銷燬造成的消耗;
  • 提高系統響應速度,當有任務到達時,通過複用已存在的執行緒,無需等待新執行緒的建立便能立即執行;
  • 方便執行緒併發數的管控。因為執行緒若是無限制的建立,可能會導致記憶體佔用過多而產生OOM
  • 節省cpu切換執行緒的時間成本(需要保持當前執行執行緒的現場,並恢復要執行執行緒的現場)。
  • 提供更強大的功能,延時定時執行緒池。(Timer vs ScheduledThreadPoolExecutor)

常用執行緒池類結構

說明:

  • 最常用的是ThreadPoolExecutor
  • 排程用的ScheduledThreadPoolExecutor
  • Executors是工具類,協助建立執行緒池

工作機制

​ 線上程池的程式設計模式下,任務是提交給整個執行緒池,而不是直接提交給某個執行緒,執行緒池在拿到任務後,就在內部尋找是否有空閒的執行緒,如果有,則將任務交給某個空閒的執行緒。一個執行緒同時只能執行一個任務,但可以同時向一個執行緒池提交多個任務。

執行緒池狀態

  • RUNNING:初始化狀態是RUNNING。執行緒池被一旦被建立,就處於RUNNING狀態,並且執行緒池中的任務數為0。RUNNING狀態下,能夠接收新任務,以及對已新增的任務進行處理。

  • SHUTDOWN:SHUTDOWN狀態時,不接收新任務,但能處理已新增的任務。呼叫執行緒池的shutdown()介面時,執行緒池由RUNNING -> SHUTDOWN。

    //shutdown後不接受新任務,但是task1,仍然可以執行完成
    ExecutorService poolExecutor = Executors.newFixedThreadPool(5);
    poolExecutor.execute(new Runnable() {
       public void run() {
           try {
               Thread.sleep(1000);
               System.out.println("finish task 1");
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    });
    poolExecutor.shutdown();
    poolExecutor.execute(new Runnable() {
       public void run() {
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    });
    System.out.println("ok");
    
  • STOP:不接收新任務,不處理已新增的任務,並且會中斷正在處理的任務。呼叫執行緒池的shutdownNow()介面時,執行緒池由(RUNNING 或SHUTDOWN ) -> STOP

    //改為shutdownNow後,任務立馬終止,sleep被打斷,新任務無法提交,task1停止
    poolExecutor.shutdownNow();
    
  • TIDYING:所有的任務已終止,ctl記錄的”任務數量”為0,執行緒池會變為TIDYING。執行緒池變為TIDYING狀態時,會執行鉤子函式terminated(),可以通過過載terminated()函式來實現自定義行為

    //自定義類,重寫terminated方法
    public class MyExecutorService extends ThreadPoolExecutor {
       public MyExecutorService(int corePoolSize, int maximumPoolSize, long
    keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
           super(corePoolSize, maximumPoolSize, keepAliveTime, unit,
    workQueue);
       }
       @Override
       protected void terminated() {
           super.terminated();
           System.out.println("treminated");
       }
       
       //呼叫 shutdownNow, ternimated方法被呼叫列印
       public static void main(String[] args) throws InterruptedException {
           MyExecutorService service = new
    MyExecutorService(1,2,10000,TimeUnit.SECONDS,new
    LinkedBlockingQueue<Runnable>(5));
           service.shutdownNow();
       }
    }
    
  • TERMINA TED:執行緒池處在TIDYING狀態時,執行完terminated()之後,就會由TIDYING ->TERMINA TED

結構說明

任務提交流程

  1. 新增任務,如果執行緒池中的執行緒數沒有達到coreSize,會建立執行緒執行任務
  2. 當達到coreSize,把任務放workQueue中
  3. 當queue滿了,未達maxsize建立心執行緒
  4. 執行緒數也達到maxsize,再新增任務會執行reject策略
  5. 任務執行完畢,超過keepactivetime,釋放超時的非核心執行緒,最終恢復到coresize大小

原始碼剖析

execute方法

//任務提交階段
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //判斷當前workers中的執行緒數量有沒有超過核心執行緒數
    if (workerCountOf(c) < corePoolSize) {
        //如果沒有則建立核心執行緒數(引數true指的就是核心執行緒)
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //如果超過核心執行緒數了 先校驗執行緒池是否正常執行後向阻塞佇列workQueue末尾新增任務
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        //再次檢查執行緒池執行狀態,若不在執行則移除該任務並且執行拒絕策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //若果沒有執行緒在執行
        else if (workerCountOf(recheck) == 0)
            //則建立一個空的worker 該worker從佇列中獲取任務執行
            addWorker(null, false);
    }
    //否則直接新增非核心執行緒執行任務 若非核心執行緒也新增失敗 則執行拒絕策略
    else if (!addWorker(command, false))
        reject(command);
}

執行緒建立:addWorker()方法

//addWorker通過cas保證了併發安全性
private boolean addWorker(Runnable firstTask, boolean core) {
    	//第一部分 計數判斷,不符合返回false
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                //判斷執行緒數,最大29位(CAPACITY=29位二進位制),所以設定執行緒池的執行緒數不是任意大的
                if (wc >= CAPACITY ||
                    //判斷工作中的核心執行緒是否大於設定的核心執行緒或者設定的最大執行緒數
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //通過cas新增 若新增失敗會一直重試 若成功則跳過結束retry
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                //再次判斷執行狀態 若執行狀態改變則繼續重試
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
		//第二部分:建立新的work放入works(一個hashSet)
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //將task任務封裝在新建的work中
            w = new Worker(firstTask);
            //獲取正在執行該任務的執行緒
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //將work加入到workers中
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //上述work新增成功了,就開始執行任務操作了
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            //如果上述新增任務失敗了,會執行移除該任務操作
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

獲取任務getTask()方法

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            //這裡判斷是否要做超時處理,這裡決定了當前執行緒是否要被釋放
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
		//檢查當前worker中執行緒數量是否超過max 並且上次迴圈poll等待超時了,則將佇列數量進行原子性減
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                //執行緒可以被釋放,那就是poll,釋放時間就是keepAliveTime
                //否則,執行緒不會被釋放,take一直阻塞在這裡,直至新任務繼續工作
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                //到這裡說明可被釋放的執行緒等待超時,已經銷燬,設定該標記,下次迴圈將執行緒數減少
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

注意點

執行緒池是如何保證不被銷燬的

當佇列中沒有任務時,核心執行緒會一直阻塞獲取任務的方法,直至獲取到任務再次執行

執行緒池中的執行緒會處於什麼狀態

WAITING , TIMED_WAITING ,RUNNABLE

核心執行緒與非核心執行緒有本質區別嗎?

答案:沒有。被銷燬的執行緒和建立的先後無關。即便是第一個被建立的核心執行緒,仍然有可能被銷燬
驗證:看原始碼,每個works在runWork的時候去getTask,在getTask內部,並沒有針對性的區分當前work是否是核心執行緒或者類似的標記。只要判斷works數量超出core,就會呼叫poll(),否則take()

鎖的分類

1)樂觀鎖/悲觀鎖

​ 樂觀鎖顧名思義,很樂觀的認為每次讀取資料的時候總是認為沒人動過,所以不去加鎖。但是在更新的時候回去對比一下原來的值,看有沒有被別人更改過。適用於讀多寫少的場景。mysql中類比version號更新java中的atomic包屬於樂觀鎖實現,即CAS(下節會詳細介紹)

​ 悲觀鎖在每次讀取資料的時候都認為其他人會修改資料,所以讀取資料的時候也加鎖,這樣別人想拿的時候就會阻塞,直到這個執行緒釋放鎖,這就影響了併發效能。適合寫操作比較多的場景。mysql中類比for update。synchronized實現就是悲觀鎖(1.6之後優化為鎖升級機制),悲觀鎖書寫不當很容易影響效能。

2)獨享鎖/共享鎖

很好理解,獨享鎖是指該鎖一次只能被一個執行緒所持有,而共享鎖是指該鎖可被多個執行緒所持有。
案例一:ReentrantLock,獨享鎖

public class PrivateLock {
   Lock lock = new ReentrantLock();
   long start = System.currentTimeMillis();
   void read() {
       lock.lock();
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }finally {
           lock.unlock();
       }
       System.out.println("read time = "+(System.currentTimeMillis() - start));
   }
   public static void main(String[] args) {
       final PrivateLock lock = new PrivateLock();
       for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
               public void run() {
                   lock.read();
               }
           }).start();
       }
   }
}

結果分析:每個執行緒結束的時間點逐個上升,鎖被獨享,一個用完下一個,依次獲取鎖

案例二:ReadWriteLock,read共享,write獨享

public class SharedLock {
   ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
   Lock lock = readWriteLock.readLock();
   long start = System.currentTimeMillis();
   void read() {
       lock.lock();
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }finally {
           lock.unlock();
       }
       System.out.println("end time = "+(System.currentTimeMillis() - start));
   }
   public static void main(String[] args) {
       final SharedLock lock = new SharedLock();
       for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
               public void run() {
                   lock.read();
               }
           }).start();
       }
   }
}

結果分析:每個執行緒獨自跑,各在100ms左右,證明是共享的

案例三:同樣是上例,換成writeLock

Lock lock = readWriteLock.writeLock();

結果分析:恢復到了1s時長,變為獨享

小結:

  • 讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
  • 獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。
3)分段鎖

從Map一家子說起....
HashMap是執行緒不安全的,在多執行緒環境下,使用HashMap進行put操作時,可能會引起死迴圈,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。

於是有了HashT able,HashT able是執行緒安全的。但是HashT able執行緒安全的策略實在不怎麼高明,將get/put所有相關操作都整成了synchronized的。

那有沒有辦法做到執行緒安全,又不這麼粗暴呢?基於分段鎖的ConcurrentHashMap誕生...

ConcurrentHashMap使用Segment(分段鎖)技術,將資料分成一段一段的儲存,Segment陣列的意義就是將一個大的table分割成多個小的table來進行加鎖,Segment陣列中每一個元素一把鎖,每一個Segment元素儲存的是HashEntry陣列+連結串列,這個和HashMap的資料儲存結構一樣。當訪問其中一個段資料被某個執行緒加鎖的時候,其他段的資料也能被其他執行緒訪問,這就使得ConcurrentHashMap不僅保證了執行緒安全,而且提高了效能。

但是這也引來一個負面影響:ConcurrentHashMap 定位一個元素的過程需要進行兩次Hash操作,第一次Hash 定位到Segment,第二次Hash 定位到元素所在的連結串列。所以Hash 的過程比普通的HashMap 要長。

備註:JDK1.8ConcurrentHashMap中拋棄了原有的Segment 分段鎖,而採用了 CAS + synchronized來保證併發安全性。

4)可重入鎖

可重入鎖指的獲取到鎖後,如果同步塊內需要再次獲取同一把鎖的時候,直接放行,而不是等待。其意義在於防止死鎖。前面使用的synchronized 和ReentrantLock 都是可重入鎖。

實現原理實現是通過為每個鎖關聯一個請求計數器和一個佔有它的執行緒。如果同一個執行緒再次請求這個鎖,計數器將遞增,執行緒退出同步塊,計數器值將遞減。直到計數器為0鎖被釋放。

場景見於父類和子類的鎖的重入(調super方法),以及多個加鎖方法的巢狀呼叫。

案例一:父子可重入

public class ParentLock {
   byte[] lock = new byte[0];
   public void f1(){
       synchronized (lock){
           System.out.println("f1 from parent");
       }
   }
}
public class SonLock extends ParentLock {
   public void f1() {
       synchronized (super.lock){
           super.f1();
           System.out.println("f1 from son");
       }
   }
   public static void main(String[] args) {
       SonLock lock = new SonLock();
       lock.f1();
   }
}

案例二:內嵌方法可重入

public class NestedLock {
   public synchronized void f1(){
       System.out.println("f1");
   }
   public synchronized void f2(){
       f1();
       System.out.println("f2");
   }
   public static void main(String[] args) {
       NestedLock lock = new NestedLock();
       //可以正常列印 f1,f2
       lock.f2();
   }
}
5)公平鎖/非公平鎖

基本概念:
公平鎖就是在併發環境中,每個執行緒在獲取鎖時會先檢視此鎖維護的等待佇列,如果為空,或者當前執行緒是等待佇列的第一個,就佔有鎖,否則就會加入到等待佇列中,直到按照FIFO的規則從佇列中取到自己。

非公平鎖與公平鎖基本類似,只是在放入佇列前先判斷當前鎖是否被執行緒持有。如果鎖空閒,那麼他可以直接搶佔,而不需要判斷當前佇列中是否有等待執行緒。只有鎖被佔用的話,才會進入排隊。在現實中想象一下游樂場旋轉木馬插隊現象......

優缺點:
公平鎖的優點是等待鎖的執行緒不會餓死,進入佇列規規矩矩的排隊,遲早會輪到。缺點是整體吞吐效率相對非公平鎖要低,等待佇列中除第一個執行緒以外的所有執行緒都會阻塞,CPU喚醒阻塞執行緒的開銷比非公平鎖大。

非公平鎖的效能要高於公平鎖,因為執行緒有機率不阻塞直接獲得鎖。ReentrantLock預設使用非公平鎖就是基於效能考量。但是非公平鎖的缺點是可能引發佇列中的執行緒始終拿不到鎖,一直排隊被餓死。

編碼方式:
很簡單,ReentrantLock支援建立公平鎖和非公平鎖(預設),想要實現公平鎖,使用new ReentrantLock(true)。

背後原理:
AQS,後面還會詳細講到。AQS中有一個state標識鎖的佔用情況,一個佇列儲存等待執行緒。
state=0表示鎖空閒。如果是公平鎖,那就看看佇列有沒有執行緒在等,有的話不參與競爭乖乖追加到尾部。如果是非公平鎖,那就直接參與競爭,不管佇列有沒有等待者。
state>0表示有執行緒佔著鎖,這時候無論公平與非公平,都直接去排隊(想搶也沒有)
備註:
因為ReentrantLock是可重入鎖,數量表示重入的次數。所以是>0而不是簡單的0和1而synchronized只能是非公平鎖

6)鎖升級

java中每個物件都可作為鎖,鎖有四種級別,按照量級從輕到重分為:無鎖、偏向鎖、輕量級鎖、重量級鎖。

如何理解呢?A佔了鎖,B就要阻塞等。但是,在作業系統中,阻塞就要儲存當前執行緒狀態,喚醒就要再恢復,這個過程是要消耗時間的...

如果A使用鎖的時間遠遠小於B被阻塞和掛起的執行時間,那麼我們將B掛起阻塞就相當的不合算。

於是出現自旋:自旋指的是鎖已經被其他執行緒佔用時,當前執行緒不會被掛起,而是在不停的試圖獲取鎖(可以理解為不停的迴圈),每迴圈一次表示一次自旋過程。顯然這種操作會消耗CPU時間,但是相比執行緒下文切換時間要少的時候,自旋划算。

而偏向鎖、輕量鎖、重量鎖就是圍繞如何使得cpu的佔用更划算而展開的。

舉個生活的例子,假設公司只有一個會議室(共享資源)
偏向鎖:
前期公司只有1個團隊,那麼什麼時候開會都能滿足,就不需要詢問和檢視會議室的佔用情況,直接進入使用
狀態。會議室門口掛了個牌子寫著A使用,A預設不需要預約(ThreadID=A)
輕量級鎖:
隨著業務發展,擴充為2個團隊,B團隊肯定不會同意A無法無天,於是當AB同時需要開會時,兩者競爭,誰搶
到誰算誰的。偏向鎖升級為輕量級鎖,但是未搶到者在門口會不停敲門詢問(自旋,迴圈),開完沒有?開完
沒有?
重量級鎖:
後來發現,這種不停敲門的方式很煩,A可能不理不睬,但是B要不停的鬧騰。於是鎖再次升級。
如果會議室被A佔用,那麼B團隊直接閉嘴,在門口安靜的等待(wait進入阻塞),直到A用完後會通知
B(notify)。

注意點:

  • 上面幾種鎖都是JVM自己內部實現,我們不需要干預,但是可以配置jvm引數開啟/關閉自旋鎖、偏
    向鎖。

  • 鎖可以升級,但是不能反向降級:偏向鎖→輕量級鎖→重量級鎖

  • 無鎖爭用的時候使用偏向鎖,第二個執行緒到了升級為輕量級鎖進行競爭,更多執行緒時,進入重量級鎖阻塞

優點 缺點 使用場景
偏向鎖 加鎖和解鎖不需要CAS操作,沒有額外的效能消耗,和執行非同步方法相比僅存在納秒級的差距 若執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗 只有一個執行緒訪問同步塊或者同步方法
輕量鎖 競爭的執行緒不會阻塞,提高了程式的響應速度 若執行緒長時間競爭不到鎖,自旋會消耗CPU 效能 執行緒交替執行同步塊或者同步方法,追求響應時間,鎖佔用時間很短,阻塞還不如自旋的場景
重量鎖 執行緒競爭不使用自旋,不會消耗CPU 執行緒阻塞,響應時間緩慢,在多執行緒下,頻繁的獲取釋放鎖,會帶來巨大的效能消耗 追求吞吐量,鎖佔用時間較長
7)互斥鎖/讀寫鎖

典型的互斥鎖:synchronized,ReentrantLock,讀寫鎖:ReadWriteLock 前面都用過了互斥鎖屬於獨享鎖,讀寫鎖裡的寫鎖屬於獨享鎖,而讀鎖屬於共享鎖

案例:互斥鎖用不好可能會失效,看一個典型的鎖不住現象!

public class ObjectLock {
   public static Integer i=0;
   public void inc(){
       synchronized (this){
           int j=i;
           try {
               Thread.sleep(100);
               j++;
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           i=j;
       }
   }
   public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
               public void run() {
                 //重點!
                   new ObjectLock().inc();
               }
           }).start();
       }
       Thread.sleep(3000);
       //理論上10才對。可是....
       System.out.println(ObjectLock.i);
   }
}

結果分析:每個執行緒內都是new物件,所以this不是同一把鎖,結果鎖不住,輸出1
1.this,換成static的i 變數試試?
2.換成ObjectLock.class 試試?
3.換成String.class
4.去掉synchronized塊,外部方法上加static synchronized

原子操作(atomic)

概念

原子(atom)本意是“不能被進一步分割的最小粒子”,而原子操作(atomic operation)意為"不可被中斷的一個或一系列操作" 。類比於資料庫事務,redis的multi。

CAS

Compare And Set(或Compare And Swap),翻譯過來就是比較並替換,CAS操作包含三個運算元——記憶體位置(V)、預期原值(A)、新值(B)。從第一視角來看,理解為:我認為位置V 應該是A,如果是A,則將B 放到這個位置;否則,不要更改,只告訴我這個位置現在的值即可。

計數器問題發生歸根結底是取值和運算後的賦值中間,發生了插隊現象,他們不是原子的操作。前面的計數器使用加鎖方式實現了正確計數,下面,基於CAS的原子類上場....

public class AtomicCounter {
   private static AtomicInteger i = new AtomicInteger(0);
   public int get(){
       return i.get();
   }
   public void inc(){
       try {
           Thread.sleep(100);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       i.incrementAndGet();
   }
   public static void main(String[] args) throws InterruptedException {
       final AtomicCounter counter = new AtomicCounter();
       for (int i = 0; i < 10; i++) {
           new Thread(new Runnable() {
               public void run() {
                   counter.inc();
               }
           }).start();
       }
       Thread.sleep(3000);
       //同樣可以正確輸出10
       System.out.println(counter.i.get());
   }
}

atomic

上面展示了AtomicInteger,關於atomic包,還有很多其他型別:

基本型別

AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;

引用型別

AtomicReference : 原子更新引用型別
AtomicReferenceFieldUpdater :原子更新引用型別的欄位
AtomicMarkableReference : 原子更新帶有標誌位的引用型別

陣列

AtomicIntegerArray:原子更新整型陣列裡的元素。
AtomicLongArray:原子更新長整型陣列裡的元素。
AtomicReferenceArray:原子更新引用型別陣列裡的元素。

欄位

AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器。
AtomicLongFieldUpdater:原子更新長整型欄位的更新器。
AtomicStampedReference:原子更新帶有版本號的引用型別。

注意

使用atomic要注意原子性的邊界,把握不好會起不到應有的效果,原子性被破壞。

public class BadAtomic {
   AtomicInteger i = new AtomicInteger(0);
   static int j=0;
   public void badInc(){
       int k = i.incrementAndGet();
       try {
           Thread.currentThread().sleep(new Random().nextInt(100));
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       j=k;
   }
   public static void main(String[] args) throws InterruptedException {
       BadAtomic atomic = new BadAtomic();
       for (int i = 0; i < 10; i++) {
           new Thread(()->{
               atomic.badInc();
           }).start();
       }
       Thread.sleep(3000);
       System.out.println(atomic.j);
   }
}

結果分析:

每次都不一樣,總之不是10
在badInc上加synchronized,問題解決

這章節目前就介紹這麼多,後續將擴充套件更多的多執行緒相關的類,以及從專案中解讀多執行緒的應用。

相關文章