long s = System.currentTimeMillis();
a(10);
b(10);
c(10);
d(10);
long e = System.currentTimeMillis();
System.out.println(e - s);
}

public void a(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void b(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void c(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void d(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

全異步執行

private void test2() {
long s = System.currentTimeMillis();
List<CompletableFuture<?>> completableFutureList = new ArrayList <>();
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
a(10);
});
completableFutureList.add(future1);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
b(10);
});
completableFutureList.add(future2);
CompletableFuture<Void> future3 = CompletableFuture.runAsync(() -> {
c(10);
});
completableFutureList.add(future3);
CompletableFuture<Void> future4 = CompletableFuture.runAsync(() -> {
d(10);
});
completableFutureList.add(future4);
CompletableFuture<?>[] futures = completableFutureList.toArray(newCompletableFuture[0]);
CompletableFuture<Void> futureAll = CompletableFuture.allOf(futures);
futureAll.join();
long e = System.currentTimeMillis();
System.out.println(e - s);
}

結果統計

測試結論

在分配了相對合理的線程池的情況下,通過以上分析,可以得出下列兩個結論:

半異步,半同步

有時候,如果方法較多,為了減少高并發時 P99 較高,我們可以讓耗時多的方法異步執行,耗時少的方法同步執行。

通過以下數據可以看出,耗時是差不多的,但可以節省不少線程資源。

總結

CompletableFuture 提供了一種優雅而強大的方式來處理并發請求和任務。然而,正如在處理高并發時使用過多的線程會導致資源浪費和效率下降一樣,使用過多的 CompletableFuture 也會導致同樣的問題。這種現象被稱為 “線程調度問題”,它會導致性能下降和吞吐量下降(P99 值較高)。

因此,我們需要在使用?CompletableFuture?時考慮實際場景和負載情況,并根據需要使用恰當的技術來優化性能。

2. 最小化事務范圍

簡要說明

首先,我們需要明確的是,事務的存在勢必會對性能產生影響,特別是在高并發的情況下,因為鎖的競爭,會帶來極大的性能損耗。因此,在處理數據交互的過程中,我們始終堅持盡可能地減少事務的范圍,從而提升接口的響應速度。

一般來說,我們可以利用@Transactional注解輕松實現事務的控制。但是,由于@Transactional?注解的最小粒度僅限于方法級別,因此,為了更好地控制事務的范圍,我們需要通過編程式事務來實現。

在編程式事務中,我們可以更靈活地控制事務的開啟和結束,以及對數據庫操作的處理。通過適當的設置事務參數和操作規則,我們可以實現事務的最小化,從而提升系統的性能和可靠性。

編程式事務模板

public interface TransactionControlService {
/**
* 事務處理
*
* @param objectLogicFunction 業務邏輯
* @param <T> result type
* @return 處理結果
* @throws Exception 業務異常信息
*/
<T> T execute(ObjectLogicFunction<T> objectLogicFunction) throws Exception;
/**
* 事務處理
*
* @param voidLogicFunction 業務邏輯
* @throws Exception 業務異常信息
*/
void execute(VoidLogicFunction voidLogicFunction) throws Exception;
}
@Service
public class TransactionControlServiceImpl implements TransactionControlService {

@Autowired
private PlatformTransactionManager platformTransactionManager;

@Autowired
private TransactionDefinition transactionDefinition;

/**
* 事務處理
*
* @param businessLogic 業務邏輯
* @param <T> result type
* @return 處理結果
* @throws Exception 業務異常信息
*/
@Override
public <T> T execute(ObjectLogicFunction<T> businessLogic) throws Exception {
TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
try {
T resp = businessLogic.logic();
platformTransactionManager.commit(transactionStatus);
return resp;
} catch (Exception e) {
platformTransactionManager.rollback(transactionStatus);
throw new Exception(e);
}
}

/**
* 事務處理
*
* @param businessLogic 業務邏輯
*/
@Override
public void execute(VoidLogicFunction businessLogic) throws Exception {
TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
try {
businessLogic.logic();
platformTransactionManager.commit(transactionStatus);
} catch (Exception e) {
platformTransactionManager.rollback(transactionStatus);
throw new Exception(e);
}
}
}
@FunctionalInterface
public interface ObjectLogicFunction<T> {

/**
* 業務邏輯處理
*
* @return 業務處理結果
* @throws BusinessException e
*/
T logic() throws BusinessException;
}
@FunctionalInterface
public interface VoidLogicFunction {

    /**
     * 業務邏輯處理
     *
     * @throws Exception e
     */
    void logic() throws Exception;
}
transactionControlService.execute(() -> {
    // 把需要事務控制的業務邏輯寫在這里即可
});

3. 緩存

簡要說明

緩存,這一在性能提升方面堪稱萬金油的技術手段,它的重要性在各種計算機應用領域中無可比擬。

緩存作為一種高效的數據讀取和寫入的優化方式,被廣泛應用于各種領域,包括電商、金融、游戲、直播等。

雖然在網絡上關于緩存的文章不勝枚舉,但要想充分發揮緩存的作用,需要針對具體的業務場景進行深入分析和探討。因此,在本節中,我們將不過多贅述緩存的具體使用方法,而是重點列舉一些使用緩存時的注意事項.

使用緩存時的注意事項

考慮到這些問題通常優化的手段

4. 合理使用線程池

簡要說明

在本文開始提到的使用 CompletableFuture 并行處理時,實際上就已經使用到線程池了,池化技術的好處,我想應該不用再過多闡述了,但關于線程池的使用還是有很多注意點的。

使用場景

異步任務

簡單來說就是某些不需要同步返回業務處理結果的場景,比如:短信、郵件等通知類業務,評論、點贊等互動性業務。

并行計算

就像 MapReduce 一樣,充分利用多線程的并行計算能力,將大任務拆分為多個子任務,最后再將所有子任務計算后的結果進行匯總,ForkJoinPool 就是 JDK 中典型的并行計算框架。

同步任務

前面講到的 CompletableFuture 使用,就是典型的同步改異步的方式,如果任務之間沒有依賴,那么就可以利用線程,同時進行處理,這樣理論上就只需要等待耗時最長的步驟結束即可(實際情況可參考 CompletableFuture 分析)。

線程池的創建

不要直接使用 Executors 創建線程池,應通過 ThreadPoolExecutor 的方式,主動明確線程池的參數,避免產生意外。

每個參數都要顯示設置,例如像下面這樣:

private static final ExecutorService executor = new ThreadPoolExecutor(
        2,
        4,
        1L,
        TimeUnit.MINUTES,
        new LinkedBlockingQueue<>(100),
        new ThreadFactoryBuilder().setNameFormat("common-pool-%d").build(),
        new ThreadPoolExecutor.CallerRunsPolicy());

參數的配置建議

CorePoolSize(核心線程數)

一般在配置核心線程數的時候,是需要結合線程池將要處理任務的特性來決定的,而任務的性質一般可以劃分為:CPU 密集型、I/O 密集型。

比較通用的配置方式如下

通過Runtime.getRuntime().availableProcessors()可以獲取核心線程數。

另外還有一個公式可以借鑒

實際上大多數線上業務所消耗的時間主要就是 I/O 等待,因此一般線程數都可以設置的多一點,比如 tomcat 中默認的線程數就是 200,所以最佳的核心線程數是需要根據特定場景,然后通過實際上線上允許結果分析后,再不斷的進行調整。

MaximumPoolSize

maximumPoolSize 的設置也是看實際應用場景,如果設置的和 corePoolSize 一樣,那就完全依靠阻塞隊列和拒絕策略來控制任務的處理情況,如果設置的比 corePoolSize 稍微大一點,那就可以更好的應對一些有突發流量產生的場景。

KeepAliveTime

由 maximumPoolSize 創建出來的線程,在經過 keepAliveTime 時間后進行銷毀,依據突發流量持續的時間來決定。

WorkQueue

那么阻塞隊列應該設置多大呢?我們知道當線程池中所有的線程都在工作時,如果再有任務進來,就會被放到阻塞隊列中等待,如果阻塞隊列設置的太小,可能很快隊列就滿了,導致任務被丟棄或者異常(由拒絕策略決定),如果隊列設置的太大,又可能會帶來內存資源的緊張,甚至 OOM,以及任務延遲時間過長。

所以阻塞隊列的大小,又是要結合實際場景來設置的。

一般會根據處理任務的速度與任務產生的速度進行計算得到一個大概的數值。

假設現在有 1 個線程,每秒鐘可以處理 10 個任務,正常情況下每秒鐘產生的任務數小于 10,那么此時隊列長度為 10 就足以。

但是如果高峰時期,每秒產生的任務數會達到 20,會持續 10 秒,且任務又不希望丟棄,那么此時隊列的長度就需要設置到 100。

監控 workQueue 中等待任務的數量是非常重要的,只有了解實際的情況,才能做出正確的決定。

在有些場景中,可能并不希望因為任務被丟進阻塞隊列而等待太長的時間,而是希望直接開啟設置的 MaximumPoolSize 線程池數來執行任務,這種情況下一般可以直接使用 SynchronousQueue 隊列來實現

ThreadFactory

通過 threadFactory 我們可以自定義線程組的名字,設置合理的名稱將有利于你線上進行問題排查。

Handler

最后拒絕策略,這也是要結合實際的業務場景來決定采用什么樣的拒絕方式,例如像過程類的數據,可以直接采用 DiscardOldestPolicy 策略。

線程池的監控

線上使用線程池時,一定要做好監控,以便根據實際運行情況進行調整,常見的監控方式可以通過線程池提供的 API,然后暴露給 Metrics 來完成實時數據統計。

監控示例

線程池自身提供的統計數據

public class ThreadPoolMonitor {

    private final static Logger log = LoggerFactory.getLogger(ThreadPoolMonitor.class);

    private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 0,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(100),
            new ThreadFactoryBuilder().setNameFormat("my_thread_pool_%d").build());

    public static void main(String[] args) {
        log.info("Pool Size: " + threadPool.getPoolSize());
        log.info("Active Thread Count: " + threadPool.getActiveCount());
        log.info("Task Queue Size: " + threadPool.getQueue().size());
        log.info("Completed Task Count: " + threadPool.getCompletedTaskCount());
    }
}

通過 micrometer API 完成統計,這樣就可以接入Prometheus

@Component
public class ThreadPoolMonitor {

    private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8, 0,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(100),
            new ThreadFactoryBuilder().setNameFormat("my_thread_pool_%d").build(), new ThreadPoolExecutor.DiscardOldestPolicy());

    /**
     * 活躍線程數
     */
    private AtomicLong activeThreadCount = new AtomicLong(0);

    /**
     * 隊列任務數
     */
    private AtomicLong taskQueueSize = new AtomicLong(0);

    /**
     * 完成任務數
     */
    private AtomicLong completedTaskCount = new AtomicLong(0);

    /**
     * 線程池中當前線程的數量
     */
    private AtomicLong poolSize = new AtomicLong(0);

    @PostConstruct
    private void init() {

        /**
         * 通過micrometer API完成統計
         *
         * gauge最典型的使用場景就是統計:list、Map、線程池、連接池等集合類型的數據
         */
        Metrics.gauge("my_thread_pool_active_thread_count", activeThreadCount);
        Metrics.gauge("my_thread_pool_task_queue_size", taskQueueSize);
        Metrics.gauge("my_thread_pool_completed_task_count", completedTaskCount);
        Metrics.gauge("my_thread_pool_size", poolSize);

        // 模擬線程池的使用
        new Thread(this::runTask).start();
    }

    private void runTask() {
        // 每5秒監控一次線程池的使用情況
        monitorThreadPoolState();
        // 模擬任務執行
        IntStream.rangeClosed(0, 500).forEach(i -> {
            // 每500毫秒,執行一個任務
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 每個處理一個任務耗時5秒
            threadPool.submit(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        });
    }

    private void monitorThreadPoolState() {
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
            activeThreadCount.set(threadPool.getActiveCount());
            taskQueueSize.set(threadPool.getQueue().size());
            poolSize.set(threadPool.getPoolSize());
            completedTaskCount.set(threadPool.getCompletedTaskCount());
        }, 0, 5, TimeUnit.SECONDS);
    }
}

線程池的資源隔離

在生產環境中,一定要注意好資源隔離的問題,盡量不要將不同類型,不同重要等級的任務放入一個線程池中,以免因為線程資源爭搶而互相影響。

5. 服務預熱

服務預熱也是很常見的一種優化手段,例如數據庫連接、線程池中的核心線程,緩存等信息可以利用服務啟動階段預先加載,從而避免請求到來后臨時構建的耗時。

下面提供一些預加載的方式

線程池

線程池本身提供了相關的 API:prestartAllCoreThreads()通過該方法可以提前將核心線程創建好,非常方便。

Web 服務

常見的如 Tomcat,其本身也用到了線程池,只是其自身已經考慮到了預加載的問題,不需要我們額外處理了。

連接池

連接池常用的一般就是數據庫連接池以及Redis連接池,大多數這些連接的客戶端也都做了連接提前加載的工作,遇到沒有預加載的參考其他客戶端方式搞一下即可。

緩存

一般本地緩存可以在每次服務啟動時預先加載好,以免出現緩存擊穿的情況。

靜態代碼塊

在服務啟動時,靜態代碼塊中的相關功能會優先被加載,可以有效避免在運行時再加載的情況。

其他擴展

預熱實際上可聊的內容很多,一般有用到池化技術的方式,都是需要預熱的,為了能夠提升響應性能,將不在內存中的數據提前查好放入內存中,或者將需要計算的數據提前計算好,這都是很容易想到的解決方式。

此外還有一些服務端在設計之初就會針對性地對一些熱點數據進行特殊處理,比如JVM中的JIT、內存分配比;OS中的page cache;MySQL中的innodb_buffer_pool等,這些一般可以通過流量預熱的方式來使其達到最佳狀態。

6. 緩存對齊

CPU的多級緩存

CPU緩存通常分為大小不等的三級緩存

來自百度百科對三級緩存分類的介紹:

一級緩存都內置在CPU內部并與CPU同速運行,可以有效的提高CPU的運行效率。一級緩存越大,CPU的運行效率越高,但受到CPU內部結構的限制,一級緩存的容量都很小。

二級緩存,它是為了協調一級緩存和內存之間的速度。cpu調用緩存首先是一級緩存,當處理器的速度逐漸提升,會導致一級緩存就供不應求,這樣就得提升到二級緩存了。二級緩存它比一級緩存的速度相對來說會慢,但是它比一級緩存的空間容量要大。主要就是做一級緩存和內存之間數據臨時交換的地方用。

三級緩存是為讀取二級緩存后未命中的數據設計的—種緩存,在擁有三級緩存的CPU中,只有約5%的數據需要從內存中調用,這進一步提高了CPU的效率。其運作原理在于使用較快速的儲存裝置保留一份從慢速儲存裝置中所讀取數據并進行拷貝,當有需要再從較慢的儲存體中讀寫數據時,緩存(cache)能夠使得讀寫的動作先在快速的裝置上完成,如此會使系統的響應較為快速。

效果演示

逐行寫入

public class CacheLine {
    public static void main(String[] args) {
        int[][] arr = new int[10000][10000];
        long s = System.currentTimeMillis();
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[i].length; j++) {
                arr[i][j] = 0;
            }
        }
        long e = System.currentTimeMillis();
        System.out.println(e-s);
    }
}

逐列寫入

public class CacheLine {
    public static void main(String[] args) {
        int[][] arr = new int[10000][10000];
        long s = System.currentTimeMillis();
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[i].length; j++) {
                arr[j][i] = 0;
            }
        }
        long e = System.currentTimeMillis();
        System.out.println(e-s);
    }
}

雖然兩種方式得到的結果是一樣的,但性能對比卻相差巨大,這就是緩存行帶來的影響。

原因分析

CPU的緩存是由多個緩存行組成的,以緩存行為基本單位,一個緩存行的大小一般為64字節,二維數組在內存中保存時,實際上是以按行遍歷的方式進行保存,比如:arr[0][0],arr[0][1],arr[1][0],arr[1][1],arr[2][0],arr[2][1]

所以當按行訪問時,是按照內存存儲的順序進行訪問,那么CPU緩存后面的元素就可以利用到,而如果是按列訪問,那么CPU的緩存是沒有用的。

緩存行對齊

public class CacheLinePadding {
    private static class Padding {
     // 一個long是8個字節,一共7個long
        // public volatile long p1, p2, p3, p4, p5, p6, p7;
    }

    private static class T extends Padding {
     // x變量8個字節,加上Padding中的變量,剛好64個字節,獨占一個緩存行。
        public volatile long x = 0L;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 10000000; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 10000000; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)  / 100000);
    }
}

同樣的含有public volatile long p1, p2, p3, p4, p5, p6, p7;這一行代碼與不含性能也相差巨大,這同樣也是因為緩存行的原因,當運行在兩個不同CPU上的兩個線程要寫入。

7. 減少對象的產生

避免使用包裝類型

因為包裝類型的創建和銷毀都會產生臨時對象,因此相比基本數據類型來說,會帶來額外的消耗。

public class Main {

    public static void main(String[] args) {
        long s = System.currentTimeMillis();
        testInteger();
        long e = System.currentTimeMillis();
        System.out.println(e - s);
        testInt();
        long e2 = System.currentTimeMillis();
        System.out.println(e2 - e);
    }

    private static void testInt() {
        int sum = 1;
        for (int i = 1; i < 50000000; i++) {
            sum++;
        }
        System.out.println(sum);
    }

    private static void testInteger() {
        Integer sum = 1;
        for (int i = 1; i < 50000000; i++) {
            sum++;
        }
        System.out.println(sum);
    }
}

兩個方法不僅執行時間相差百倍,在CPU和內存的消耗上Integer也明顯弱于int。

Integer內存和CPU都能看到明顯的波動

int幾乎沒波動

使用不可變對象

最為典型的案例就是String,我想應該不會有人去通過new的方式再去構建一個String字符串了吧!

String str = new String("abc"); 
String str = "abc";

同時,在實現字符串連接時通常使用StringBuilder或StringBuffer,這樣可以避免使用連接符,導致每次都創建新的字符串對象。

靜態方法

靜態對象

Boolean.valueOf("true");

public static Boolean valueOf(String s) {
    return parseBoolean(s) ? TRUE : FALSE;
}

public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

靜態工廠(單例模式)

public class StaticSingleton {
    private static class StaticHolder {
        public static final StaticSingleton INSTANCE = new StaticSingleton();
    }

    public static StaticSingleton getInstance() {
        return StaticHolder.INSTANCE;
    }
}

枚舉

public enum EnumSingleton { INSTANCE; }

視圖

視圖是返回引用的一種方式。

map的keySet方法,實際上每次返回的都是同一個對象的引用。

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

對象池

對象池可以有效減少頻繁的對象創建和銷毀的過程,一般情況下如果每次創建對象的過程較為復雜,且對象占用空間又比較大,那么就建議使用對象池的方式來優化。

使用示例

org.apache.commons提供了對象池的工具類,可以直接拿來使用<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>

池化的對象

@Data
public class Cache {
    private byte[] size;
}

池化對象工廠

public class CachePoolObjectFactory extends BasePooledObjectFactory<Cache> {

    @Override
    public Cache create() {
        Cache cache = new Cache();
        cache.setSize(new byte[1024 * 1024 * 16]);
        return cache;
    }

    @Override
    public PooledObject<Cache> wrap(Cache cache) {
        return new DefaultPooledObject<>(cache);
    }
}

對象池工具

public enum CachePoolUtil {

    INSTANCE;

    private GenericObjectPool<Cache> objectPool;

    CachePoolUtil() {
        GenericObjectPoolConfig<Cache> poolConfig = new GenericObjectPoolConfig<>();
        // 對象池中最大對象數
        poolConfig.setMaxTotal(50);
        // 對象池中最小空閑對象數
        poolConfig.setMinIdle(20);
        // 對象池中最大空閑對象數
        poolConfig.setMaxIdle(20);
        // 獲取對象最大等待時間 默認 -1 一直等待
        poolConfig.setMaxWait(Duration.ofSeconds(3));
        // 創建對象工廠
        CachePoolObjectFactory objectFactory = new CachePoolObjectFactory();
        // 創建對象池
        objectPool = new GenericObjectPool<>(objectFactory, poolConfig);
    }

    /**
     * 從對象池中取出一個對象
     */
    public Cache borrowObject() throws Exception {
        return objectPool.borrowObject();
    }

    public void returnObject(Cache cache) {
        // 將對象歸還給對象池
        objectPool.returnObject(cache);
    }

    /**
     * 獲取活躍的對象數
     */
    public int getNumActive() {
        return objectPool.getNumActive();
    }

    /**
     * 獲取空閑的對象數
     */
    public int getNumIdle() {
        return objectPool.getNumIdle();
    }

}
public class Main {

public static void main(String[] args) {
CachePoolUtil cachePoolUtil = CachePoolUtil.INSTANCE;
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
while (true) {
Thread.sleep(100);

// 使用對象池
Cache cache = cachePoolUtil.borrowObject();
m(cache);
cachePoolUtil.returnObject(cache);

// 不使用對象池
//Cache cache = new Cache();
//cache.setSize(new byte[1024 * 1024 * 2]);
//m(cache);

? }

? }
? }).start();
? }
}

// 無特殊作用
public static void m(Cache cache) {
? if (cache.getSize().length < 10) {
? System.out.println(cache);
? }
}
}

使用對象池

不適用對象池

8. 并發處理

鎖的粒度控制

并發場景下就要考慮線程安全的問題,常見的解決方式:volatile、CAS、自旋鎖、對象鎖、類鎖、分段鎖、讀寫鎖,理論上來說,鎖的粒度越小,并行效果就越高。

volatile

volatile是Java中的一個關鍵字,用于修飾變量。它的作用是保證被volatile修飾的變量在多線程環境下的可見性和禁止指令重排序。

volatile雖然不能保證原子性,但如果對共享變量是純賦值或讀取的操作,那么因為volatile保證了可見性,因此也是可以實現線程安全的。

CAS

compare and swap(比較并交換),CAS主要有三個參數,

當且僅當V等于A時,就將A更新為B,否則什么都不做。V和A的比較是一個原子性操作保證線程安全。

Random通過cas的方式保證了線程安全,但在高并發下很有可能會失敗,造成頻繁的重試。

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}

ThreadLocalRandom進行了優化,其主要方式就是分段,通過讓每個線程擁有獨立的存儲空間,這樣即保證了線程安全,同時效率也不會太差。

public static ThreadLocalRandom current() {
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
U.putLong(t, SEED, seed);
U.putInt(t, PROBE, probe);
}
public int nextInt() {
return mix32(nextSeed());
}
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
U.putLong(t = Thread.currentThread(), SEED,
r = U.getLong(t, SEED) + GAMMA);
return r;
}

對象鎖、類鎖

主要就是通過synchronized實現,是最基礎的鎖機制。

自旋鎖

在自旋鎖中,當一個操作需要訪問一個共享資源時,它會檢查這個資源是否被其他操作占用。如果是,它會一直等待,直到資源被釋放。在等待期間,這個操作會進入一個自旋狀態,也就是不會被系統掛起,但是也不會繼續執行其他任務。當資源被釋放后,這個操作會立即返回并繼續執行下一步操作。

自旋鎖是一種簡單而有效的同步機制,自旋鎖的優點是減少線程上下文切換的開銷,但是它也有一些缺點。由于它需要一直進行自旋操作,所以會消耗一定的CPU資源。因此,在使用自旋鎖時需要仔細考慮并發問題和性能問題。

分段鎖

在分段鎖的模型中,共享數據被分割成若干個段,每個段都被一個鎖所保護,同時只有一個線程可以在同一時刻對同一段進行加鎖和解鎖操作。這種鎖機制可以降低鎖的競爭,提高并發訪問的效率。

ConcurrentHashMap的設計就是采用分段鎖的思想,其會按照map中的table capacity(默認16)來劃分,也就是說每個線程會鎖1/16的數據段,這樣一來就大大提升了并發訪問的效率。

讀寫鎖

讀寫鎖主要根據大多數業務場景都是讀多寫少的情況,在讀數據時,無論多少線程同時訪問都不會有安全問題,所以在讀數據的時候可以不加鎖,不過一旦有寫請求時就需要加鎖了。

典型的如:ReentrantReadWriteLock

寫時復制

寫時復制最大的優勢在于,在寫數據的過程時,不影響讀,可以理解為讀的是數據的副本,而只有當數據真正寫完后才會替換副本,當副本特別大、寫數據過程比較漫長時,寫時復制就特別有用了。

CopyOnWriteArrayListCopyOnWriteArraySet就是集合操作時,為保證線程安全,使用寫時復制的實現

public E get(int index) {
    return elementAt(getArray(), index);
}
final Object[] getArray() {
    return array;
}
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}
final void setArray(Object[] a) {
    array = a;
}

寫時復制也存在兩個問題,可以看到在add方法時使用了synchronized,也就是說當存在大量的寫入操作時,效率實際上是非常低的,另一個問題就是需要copy一份一模一樣的數據,可能會造成內存的異常波動,因此寫時復制實際上適用于讀多寫少的場景。

對比說明

public class ThreadSafeSet {
public static void main(String[] args) throws InterruptedException {

//Set<String> set = ConcurrentHashMap.newKeySet();

//CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet();

readMoreWriteLess(set);

System.out.println("==========華麗的分隔符==========");

//set = ConcurrentHashMap.newKeySet();

//set = new CopyOnWriteArraySet();

writeMoreReadLess(set);
}

private static void writeMoreReadLess(Set<String> set) throws InterruptedException {
//測20組
for (int k = 1; k <= 20; k++) {
CountDownLatch countDownLatch = new CountDownLatch(10);
long s = System.currentTimeMillis();
//創建9個線程,每個線程向set中寫1000條數據
for (int i = 0; i < 9; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
set.add(UUID.randomUUID().toString());
}
countDownLatch.countDown();
}).start();
}

//創建1個線程,每個線程從set中讀取所有數據,每個線程一共讀取10次。
for (int i = 0; i < 1; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
iterator.next();
}
}
countDownLatch.countDown();
}).start();
}
//阻塞,直到10個線程都執行結束
countDownLatch.await();
long e = System.currentTimeMillis();
System.out.println("寫多讀少:第" + k + "次執行耗時:" + (e - s) + "毫秒" + ",容器中元素個數為:" + set.size());
}
}

private static void readMoreWriteLess(Set<String> set) throws InterruptedException {
//測20組
for (int k = 1; k <= 20; k++) {
CountDownLatch countDownLatch = new CountDownLatch(10);
long s = System.currentTimeMillis();
//創建1個線程,每個線程向set中寫10條數據
for (int i = 0; i < 1; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
set.add(UUID.randomUUID().toString());
}
countDownLatch.countDown();
}).start();
}

//創建9個線程,每個線程從set中讀取所有數據,每個線程一共讀取100萬次。
for (int i = 0; i < 9; i++) {
new Thread(() -> {
for (int j = 0; j < 1000000; j++) {
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
iterator.next();
}
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
long e = System.currentTimeMillis();
System.out.println("讀多寫少:第" + k + "次執行耗時:" + (e - s) + "毫秒" + ",容器中元素個數為:" + set.size());
}
}
}

經過測試可以發現在讀多寫少時CopyOnWriteArraySet會明顯優于ConcurrentHashMap.newKeySet(),但在寫多讀少時又會明顯弱于ConcurrentHashMap.newKeySet()

當然使用CopyOnWriteArraySet還需要注意一點,寫入的數據可能不會被及時的讀取到,因為遍歷的是讀取之前獲取的快照。

這段代碼可以測試CopyOnWriteArraySet寫入數據不能被及時讀取到的問題。

public class COWSetTest {
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet();
        new Thread(() -> {
            try {
                set.add(1);
                System.out.println("第一個線程啟動,添加了一個元素,睡100毫秒");
                Thread.sleep(100);
                set.add(2);
                set.add(3);
                System.out.println("第一個線程添加了3個元素,執行結束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        //保證讓第一個線程先執行
        Thread.sleep(1);

        new Thread(() -> {
            try {
                System.out.println("第二個線程啟動了!睡200毫秒");
                //Thread.sleep(200);//如果在這邊睡眠,可以獲取到3個元素
                Iterator<Integer> iterator = set.iterator();//生成快照
                Thread.sleep(200);//如果在這邊睡眠,只能獲取到1個元素
                while (iterator.hasNext()) {
                    System.out.println("第二個線程開始遍歷,獲取到元素:" + iterator.next());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

    }
}

9. 異步

異步是提升系統響應能力的重要手段之一,異步思想的應用也非常的廣泛,常見的有:線程、MQ、事件通知、響應式編程等方式,有些概念在前面的章節中也涉及到了,異步最核心的思想就是,先快速接收,后查詢結果,比如:如果接口處理時間較長,那么可以優先響應中間狀態(處理中),然后提供回調和查詢接口,這樣就可以大大提升接口的吞吐量!

10. for循環優化

減少循環

通常可以通過一些高效的算法或者數據結構來減少循環次數,尤其當出現嵌套循環時要格外小心。常見的方式比如:有序的查找可以用二分,排序可以用快排,檢索可以構建Hash索引等等。

批量獲取

優化前:每次查詢一次數據庫

for(String userId : userIds){
    User user = userMapper.queryById(userId);
    if(user.getName().equals("xxx")){
        // ...
    }
    
}

優化后:先批量查詢出來,再處理

Map<String, User> userMap = userMapper.queryByIds(userIds);
for(String userId : userIds){
    User user = userMap.get(userId);
    if(user.getName().equals("xxx")){
        // ...
    }
}

緩存結果

優化前:每次都要根據每個用戶的roleId去數據庫查詢一次。

Map<String, User> userMap = userMapper.queryByIds(userIds);
for(String userId : userIds){
    User user = userMap.get(userId);
    Role role = roleMapper.queryById(user.getRoleId());
}

優化后:每次根據roleId查詢過以后就暫記下來,后面再遇到相同roleId時即可直接獲取,這比較適用于一次循環中roleId重復次數較多的場景。

Map<String, User> userMap = userMapper.queryByIds(userIds);
Map<String, Role> roleMap = new HashMap<>();
for(String userId : userIds){
    User user = userMap.get(userId);
    Role role = roleMap.get(user.getRoleId());
    if(role == null){
        role = roleMapper.queryById(user.getRoleId());
        roleMap.put(user.getRoleId(), role);
    }
}

并行處理

典型的如parallelStream

Integer sum = numbers.parallelStream().reduce(0, Integer::sum);

11. 減少網絡傳輸的體積

精簡字段

1.數據庫查詢時要避免頻繁查詢大文本字段,常見的如下面幾種:select url, describe, remark from t

2.接口傳輸時同樣要注意盡量減少內容傳輸的大小。

3.精簡字段除了通過減少不必要的字段傳輸之外,也可以通過改變數據結構,數據類型來實現。

數據傳輸格式

常用的如JSON,語法簡單,相比XML來說傳輸體積更小,解析更快,但如果需要頻繁傳輸大量數據時,使用protobuf則更會更加高效,因為其采用結構化的數據描述語言,并使用二進制編碼,因為體積更小,速度更快。

壓縮

常見的數據壓縮方式如:GZIP、zlib,而zip常用于文件壓縮。

借助Hutool工具包,可以看下壓縮的效果

gzip壓縮

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
System.out.println("壓縮前:" + sb.toString().getBytes().length);
byte[] compressedBytes = ZipUtil.gzip(sb.toString(), CharsetUtil.UTF_8);
System.out.println("壓縮后:" + compressedBytes.length);
String str = ZipUtil.unGzip(compressedBytes, CharsetUtil.UTF_8);
System.out.println("壓縮還原:" + str.getBytes().length);
壓縮前:2890
壓縮后:1474
壓縮還原:2890

zlib壓縮

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
System.out.println("壓縮前:" + sb.toString().getBytes().length);
byte[] compressedBytes = ZipUtil.zlib(sb.toString(), CharsetUtil.UTF_8, 1);
System.out.println("壓縮后:" + compressedBytes.length);
String str = ZipUtil.unZlib(compressedBytes, CharsetUtil.UTF_8);
System.out.println("壓縮還原:" + str.getBytes().length);
壓縮前:2890
壓縮后:1518
壓縮還原:2890

12. 減少服務之間的依賴

依賴越多,不但會給服務的穩定性、可靠性造成影響,同時也會成為性能提升的瓶頸,因此我們在設計之初就應當充分考慮到這個問題,通過合理的手段來減少服務之間的依賴。

鏈路治理

通過合理的微服務劃分,可以有效的減少鏈路上的依賴,鏈路調用之間要避免出現重復調用,循環依賴,以及上、下層級互相調用的情況。

重復調用

循環依賴

服務上、下層級混亂,互相調用

數據冗余

數據冗余是指將非自身維護的數據通過某種手段保存下來,以便在之后使用時避免多次發起數據請求,從而實現減少服務依賴的手段。

常見的方式如:通用的基礎數據,字典數據等各個需求方可復制一份存在本地;建立寬表,冗余部分數據,減少關聯查詢。

結果緩存

將需要頻繁使用的結果存儲在緩存服務中,也是有效減少服務依賴的方式之一。

消息隊列

消息隊列天然就有簡化系統復雜性的作用,它通過異步的方式將任務與任務之間的關系進行解耦,也就達到了減少服務之間依賴的效果。

文章轉自微信公眾號@一安未來

上一篇:

如何設計API?接口的統一格式指南

下一篇:

18個最佳API設計規范實踐
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費