使用雙非同步後,如何保證資料一致性?

ITPUB社群發表於2024-01-23

來源:哪吒程式設計

unsetunset一、前情提要unsetunset

在上一篇文章中,我們使用雙非同步後,從 191s 最佳化到 2s,有個小夥伴在評論區問我,如何保證插入後資料的一致性呢?

很簡單,透過對比Excel檔案行數和入庫數量是否相等即可。

那麼,如何獲取非同步執行緒的返回值呢?

使用雙非同步後,如何保證資料一致性?

unsetunset二、透過Future獲取非同步返回值unsetunset

我們可以透過給非同步方法新增Future返回值的方式獲取結果。

FutureTask 除了實現 Future 介面外,還實現了 Runnable 介面。因此,FutureTask 可以交給 Executor 執行,也可以由呼叫執行緒直接執行FutureTask.run()。

1、FutureTask 是基於 AbstractQueuedSynchronizer實現的

AbstractQueuedSynchronizer簡稱AQS,它是一個同步框架,它提供通用機制來原子性管理同步狀態、阻塞和喚醒執行緒,以及 維護被阻塞執行緒的佇列。基於 AQS 實現的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、 CountDownLatch 和 FutureTask。

基於 AQS實現的同步器包含兩種操作:

  1. acquire,阻塞呼叫執行緒,直到AQS的狀態允許這個執行緒繼續執行,在FutureTask中,get()就是這個方法;
  2. release,改變AQS的狀態,使state變為非阻塞狀態,在FutureTask中,可以透過run()和cancel()實現。

2、FutureTask執行流程

使用雙非同步後,如何保證資料一致性?
  1. 執行@Async非同步方法;
  2. 建立新執行緒async-executor-X,執行Runnable的run()方法,(FutureTask實現RunnableFuture,RunnableFuture實現Runnable);
  3. 判斷狀態state;
  • 如果未新建或者不處於AQS,直接返回;
  • 否則進入COMPLETING狀態,執行非同步執行緒程式碼;
  • 如果執行cancel()方法改變AQS的狀態時,會喚醒AQS等待佇列中的第一個執行緒執行緒async-executor-1;
  • 執行緒async-executor-1被喚醒後
    • 將自己從AQS佇列中移除;
    • 然後喚醒next執行緒async-executor-2;
    • 改變執行緒async-executor-1的state;
    • 等待get()執行緒取值。
  • next等待執行緒被喚醒後,迴圈執行緒async-executor-1的步驟
    • 被喚醒
    • 從AQS佇列中移除
    • 喚醒next執行緒
    • 改變非同步執行緒狀態
  • 新建執行緒async-executor-N,監聽非同步方法的state
    • 如果處於EXCEPTIONAL以上狀態,丟擲異常;
    • 如果處於COMPLETING狀態,加入AQS佇列等待;
    • 如果處於NORMAL狀態,返回結果;

    3、get()方法執行流程

    get()方法透過判斷狀態state觀測非同步執行緒是否已結束,如果結束直接將結果返回,否則會將等待節點扔進等待佇列自旋,阻塞住執行緒。

    自旋直至非同步執行緒執行完畢,獲取另一邊的執行緒計算出結果或取消後,將等待佇列裡的所有節點依次喚醒並移除佇列。

    使用雙非同步後,如何保證資料一致性?
    1. 如果state小於等於COMPLETING,表示任務還在執行中;
    • 計算超時時間;
    • 如果超時,則從等待佇列中移除等待節點WaitNode,返回當前狀態state;
    • 阻塞佇列nanos毫秒。
    • 如果已有等待節點WaitNode,將執行緒置空;
    • 返回當前狀態;
    • 如果執行緒被中斷,從等待佇列中移除等待節點WaitNode,丟擲中斷異常;
    • 如果state大於COMPLETING;
    • 如果任務正在執行,讓出時間片;
    • 如果還未構造等待節點,則new一個新的等待節點;
    • 如果未入佇列,CAS嘗試入隊;
    • 如果有超時時間引數;
    • 否則阻塞佇列;
  • 如果state大於COMPLETING;
    • 如果執行完畢,返回結果;
    • 如果大於等於取消狀態,則丟擲異常。

    很多小朋友對讀原始碼,嗤之以鼻,工作3年、5年,還是沒認真讀過任何原始碼,覺得讀了也沒啥用,或者讀了也看不懂~

    其實,只要把原始碼的執行流程透過畫圖的形式呈現出來,你就會幡然醒悟,原來是這樣的~

    簡而言之:

    1. 如果非同步執行緒還沒執行完,則進入CAS自旋;

    2. 其它執行緒獲取結果或取消後,重新喚醒CAS佇列中等待的執行緒;

    3. 再透過get()判斷狀態state;

    4. 直至返回結果或(取消、超時、異常)為止。

    unsetunset三、FutureTask原始碼具體分析unsetunset

    1、FutureTask原始碼

    透過定義整形狀態值,判斷state大小,這個思想很有意思,值得學習。

    public interface RunnableFuture<Vextends RunnableFuture<V{
        /**
         * Sets this Future to the result of its computation
         * unless it has been cancelled.
         */

        void run();
    }
    public class FutureTask<Vimplements RunnableFuture<V{

     // 最初始的狀態是new 新建狀態
     private volatile int state;
        private static final int NEW          = 0// 新建狀態
        private static final int COMPLETING   = 1// 完成中
        private static final int NORMAL       = 2// 正常執行完
        private static final int EXCEPTIONAL  = 3// 異常
        private static final int CANCELLED    = 4// 取消
        private static final int INTERRUPTING = 5// 正在中斷
        private static final int INTERRUPTED  = 6// 已中斷

     public V get() throws InterruptedException, ExecutionException {
         int s = state;
         // 任務還在執行中
         if (s <= COMPLETING)
             s = awaitDone(false0L);
         return report(s);
     }
     
     private int awaitDone(boolean timed, long nanos)
            throws InterruptedException 
    {
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            WaitNode q = null;
            boolean queued = false;
            for (;;) {
             // 執行緒被中斷,從等待佇列中移除等待節點WaitNode,丟擲中斷異常
                if (Thread.interrupted()) {
                    removeWaiter(q);
                    throw new InterruptedException();
                }

                int s = state;
                // 任務已執行完畢或取消
                if (s > COMPLETING) {
                 // 如果已有等待節點WaitNode,將執行緒置空
                    if (q != null)
                        q.thread = null;
                    return s;
                }
                // 任務正在執行,讓出時間片
                else if (s == COMPLETING) // cannot time out yet
                    Thread.yield();
                // 還未構造等待節點,則new一個新的等待節點
                else if (q == null)
                    q = new WaitNode();
                // 未入佇列,CAS嘗試入隊
                else if (!queued)
                    queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                         q.next = waiters, q);
                // 如果有超時時間引數
                else if (timed) {
                 // 計算超時時間
                    nanos = deadline - System.nanoTime();
                    // 如果超時,則從等待佇列中移除等待節點WaitNode,返回當前狀態state
                    if (nanos <= 0L) {
                        removeWaiter(q);
                        return state;
                    }
                    // 阻塞佇列nanos毫秒
                    LockSupport.parkNanos(this, nanos);
                }
                else
                 // 阻塞佇列
                    LockSupport.park(this);
            }
        }
        
     private V report(int s) throws ExecutionException {
      // 獲取outcome中記錄的返回結果
            Object x = outcome;
            // 如果執行完畢,返回結果
            if (s == NORMAL)
                return (V)x;
                // 如果大於等於取消狀態,則丟擲異常
            if (s >= CANCELLED)
                throw new CancellationException();
            throw new ExecutionException((Throwable)x);
        }
    }

    2、將非同步方法的返回值改為Future<Integer>,將返回值放到new AsyncResult<>();中;

    @Async("async-executor")
    public void readXls(String filePath, String filename) {
        try {
         // 此程式碼為簡化關鍵性程式碼
            List<Future<Integer>> futureList = new ArrayList<>();
            for (int time = 0; time < times; time++) {
                Future<Integer> sumFuture = readExcelDataAsyncFutureService.readXlsCacheAsync();
                futureList.add(sumFuture);
            }
        }catch (Exception e){
            logger.error("readXlsCacheAsync---插入資料異常:",e);
        }
    }
    @Async("async-executor")
    public Future<Integer> readXlsCacheAsync() {
        try {
            // 此程式碼為簡化關鍵性程式碼
            return new AsyncResult<>(sum);
        }catch (Exception e){
            return new AsyncResult<>(0);
        }
    }

    3、透過Future<Integer>.get()獲取返回值:

    public static boolean getFutureResult(List<Future<Integer>> futureList, int excelRow){
        int[] futureSumArr = new int[futureList.size()];
        for (int i = 0;i<futureList.size();i++) {
            try {
                Future<Integer> future = futureList.get(i);
                while (true) {
                    if (future.isDone() && !future.isCancelled()) {
                        Integer futureSum = future.get();
                        logger.info("獲取Future返回值成功"+"----Future:" + future
                                + ",Result:" + futureSum);
                        futureSumArr[i] += futureSum;
                        break;
                    } else {
                        logger.info("Future正在執行---獲取Future返回值中---等待3秒");
                        Thread.sleep(3000);
                    }
                }
            } catch (Exception e) {
                logger.error("獲取Future返回值異常: ", e);
            }
        }
        
        boolean insertFlag = getInsertSum(futureSumArr, excelRow);
        logger.info("獲取所有非同步執行緒Future的返回值成功,Excel插入結果="+insertFlag);
        return insertFlag;
    }

    4、這裡也可以透過新執行緒+Future獲取Future返回值

    不過感覺多此一舉了,就當練習Future非同步取返回值了~

    public static Future<Boolean> getFutureResultThreadFuture(List<Future<Integer>> futureList, int excelRow) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        final boolean[] insertFlag = {false};
        service.execute(new Runnable() {
            public void run() {
                try {
                    insertFlag[0] = getFutureResult(futureList, excelRow);
                } catch (Exception e) {
                    logger.error("新執行緒+Future獲取Future返回值異常: ", e);
                    insertFlag[0] = false;
                }
            }
        });
        service.shutdown();
        return new AsyncResult<>(insertFlag[0]);
    }

    獲取非同步執行緒結果後,我們可以透過新增事務的方式,實現Excel入庫操作的資料一致性。

    但Future會造成主執行緒的阻塞,這個就很不友好了,有沒有更優解呢?

    來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024420/viewspace-3004755/,如需轉載,請註明出處,否則將追究法律責任。

    相關文章