并發(fā)編程-學(xué)習(xí)總結(jié)(上)

推薦課程
目錄

1、線程基礎(chǔ)

1.1、線程實(shí)現(xiàn)方法

1.2、如何正確停止線程

1.3、Java線程的六種狀態(tài)

1.4、wait/notify/notifyAll注意事項(xiàng)

1.4.1、為什么 wait 、notify、notifyAll必須在 synchronized 保護(hù)的同步代碼中使用?

1.4.2、為什么 wait/notify/notifyAll 被定義在 Object 類中,而 sleep 定義在 Thread 類中?

1.4.3、wait/notify 和 sleep 方法的異同?

1.5、實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式的方法

1.5.1、BlockingQueue

1.5.2、Condition

1.5.3、 wait/notify

2、線程安全

2.1、3種線程安全問(wèn)題

2.1.1、運(yùn)行結(jié)果問(wèn)題

2.1.2、發(fā)布和初始化導(dǎo)致線程安全問(wèn)題

2.1.3、活躍性問(wèn)題

2.2、哪些場(chǎng)景需要注意線程安全問(wèn)題

2.3、為什么多線程會(huì)帶來(lái)性能問(wèn)題

3、線程池

3.1、線程池的好處

3.2、創(chuàng)建線程池各參數(shù)的含義

3.3、線程池執(zhí)行流程

3.4、默認(rèn)四種拒絕策略

3.4.1、拒絕時(shí)機(jī)

3.4.2、拒絕策略

3.4、常見(jiàn)線程池

3.5、ForkJoinPool

3.5.1、功能

3.5.2、父類

3.5.3、自定義Task類

3.5.4、創(chuàng)建線程池

3.5.5、任務(wù)運(yùn)行原理

3.6、線程池常用的阻塞隊(duì)列

3.6、為什么不要通過(guò)Executors創(chuàng)建線程池

3.7、合適的線程數(shù)量

3.7.1、CPU密集型任務(wù):

3.7.2、耗時(shí)IO型任務(wù):

3.8、如何正確關(guān)閉線程池

3.9、如何設(shè)計(jì)線程池

4、鎖

4.1、鎖分類

4.1.1、偏向鎖/輕量級(jí)鎖/重量級(jí)鎖

4.1.2、可重入鎖/非可重復(fù)鎖

4.1.3、共享鎖/獨(dú)占鎖

4.1.4、公平鎖/非公平鎖

4.1.5、悲觀鎖/樂(lè)觀鎖

4.1.6、自旋鎖/非自旋鎖

4.1.7、可中斷鎖/不可中斷鎖

4.2、synchronized的monitor鎖

4.2.1、同步代碼塊

4.2.2、同步方法

4.3、如何選擇synchronized和Lock

4.3.1、相同點(diǎn)

4.3.2、不同點(diǎn)

4.3.3、如何選擇

4.4、Lock的常用方法(ReentrantLock)

4.5、公平鎖和非公平鎖

4.5.1、為什么有非公平鎖

4.6、讀寫鎖ReadWriteLock

4.7、讀寫鎖升降級(jí)

4.7.1、非公平鎖下的讀寫操作

4.7.2、讀寫鎖的降級(jí)

4.7.3、不支持讀寫鎖的升級(jí):讀鎖->寫鎖。

4.7.4、鎖降級(jí)中讀鎖的獲取是否必要呢?

4.8、自旋鎖

4.9、JVM對(duì)synchronized的優(yōu)化

4.9.1、自旋

4.9.2、鎖消除

4.9.3、鎖粗化

4.9.4、鎖升級(jí)策略

4.10、鎖升級(jí)策略

4.10.1、無(wú)鎖

4.10.2、無(wú)鎖->偏向鎖

4.10.3、偏向鎖升級(jí)為輕量級(jí)鎖

4.10.4、輕量級(jí)鎖->重量級(jí)鎖

4.10.5、適用場(chǎng)景

4.10.6、鎖的優(yōu)缺點(diǎn)比較

5、并發(fā)容器

5.1、HashMap為什么線程不安全

5.2、ConcurrentHashMap

5.3、為什么Map桶中超過(guò)8個(gè)才轉(zhuǎn)為紅黑樹(shù)

5.4、ConcurrentHashMap 和 Hashtable 的區(qū)別

5.5、CopyOnWriteArrayList

5.5.1、適用場(chǎng)景

5.5.2、特點(diǎn)

5.5.3、原理

5.5.4、缺陷

6、阻塞隊(duì)列(BlockingQueue )

6.1、阻塞隊(duì)列

6.2、常用方法

6.3、常見(jiàn)的阻塞隊(duì)列

6.4、并發(fā)安全原理

6.5、如何選擇阻塞隊(duì)列

6.5.1、線程池對(duì)應(yīng)的阻塞隊(duì)列

6.5.2、如何選擇

7、原子類

7.1、原子類如何利用CAS保證線程安全

7.1.1、原理

7.1.2、原子類

7.2、如何解決AtomicInteger在高并發(fā)下的性能問(wèn)題

7.2.1、原因

7.2.2、解決方案

7.2.3、適用場(chǎng)景

7.3、原子類和volatile

7.4、AtomicInteger 和 synchronized 的異同點(diǎn)?

7.5、Java 8 中 Adder 和 Accumulator 有什么區(qū)別?

8、ThreadLocal

8.1、適用場(chǎng)景

8.2、ThreadLocal是用來(lái)解決共享資源的多線程訪問(wèn)問(wèn)題嗎?

8.3、多個(gè)ThreadLocal在Thread的threadlocals里是如何存儲(chǔ)的?

8.4、內(nèi)存泄露

8.4.1、key的內(nèi)存泄露

8.4.2、value的內(nèi)存泄露

8.4.3、避免內(nèi)存泄露

1、線程基礎(chǔ)

1.1、線程實(shí)現(xiàn)方法

  • 實(shí)現(xiàn)Runnable接口(無(wú)返回值)
  • 繼承Thread類
  • 線程池創(chuàng)建線程
  • 實(shí)現(xiàn)Callable接口(有返回值)

1.2、如何正確停止線程

  • interrupt()方法:請(qǐng)求中斷,設(shè)置中斷標(biāo)志(推薦)

若線程處于正常狀態(tài),則會(huì)將該線程的中斷標(biāo)志設(shè)置為true,之后線程將繼續(xù)正常運(yùn)行,不受影響。
若線程處于被阻塞狀態(tài),或由正常狀態(tài)變?yōu)樽枞麪顟B(tài)(sleep()、wait()),線程將立刻退出被阻塞狀態(tài),并拋出一個(gè)InterruptedException異常??梢酝ㄟ^(guò)捕獲異常然后return。

  • stop():停止線程(不推薦)

直接把線程停止,影響數(shù)據(jù)完整性

  • suspend():暫停線程(不推薦)

暫停線程,但不會(huì)釋放鎖,可能會(huì)導(dǎo)致死鎖

  • resume():恢復(fù)因suspend掛起的線程(不推薦)
  • volatile變量(推薦)

在存在循環(huán)的線程run方法中進(jìn)行判斷是否退出線程
但在某些特殊的情況下,比如線程被長(zhǎng)時(shí)間阻塞的情況,就無(wú)法及時(shí)感受中斷,所以 volatile 是不夠全面的停止線程的方法。

1.3、Java線程的六種狀態(tài)

[圖片上傳失敗...(image-2358ea-1689324444603)]

  • New(新建):已創(chuàng)建線程,但還未執(zhí)行start()
  • Runnable(就緒):對(duì)應(yīng)操作系統(tǒng)的Running和Ready狀態(tài)(CPU分配時(shí)間片)
  • Blocked(被阻塞):進(jìn)入 synchronized 保護(hù)的代碼時(shí)沒(méi)有搶到 monitor 鎖
  • Waiting(等待):執(zhí)行沒(méi)有設(shè)置timeout參數(shù)的Object.wait()、Thread.join()方法
  • Timed Waiting(計(jì)時(shí)等待):執(zhí)行設(shè)置了timeout參數(shù)的Object.wait()、Thread.join()、Thread.sleep()方法
  • Terminated(終止):run()方法執(zhí)行完畢;或者出現(xiàn)未捕獲的異常,導(dǎo)致意外終止

注意:waiting和Timed Waiting兩種狀態(tài)的線程,在被其他線程使用notify/notifyAll喚醒時(shí),會(huì)先進(jìn)入到Blocked狀態(tài)。因?yàn)閱拘?Waiting 線程的線程如果調(diào)用 notify() 或 notifyAll(),要求必須首先持有該 monitor 鎖,所以處于 Waiting 狀態(tài)的線程被喚醒時(shí)拿不到該鎖,就會(huì)進(jìn)入 Blocked 狀態(tài),直到執(zhí)行了 notify()/notifyAll() 的喚醒它的線程執(zhí)行完畢并釋放 monitor 鎖,才可能輪到它去搶奪這把鎖,如果它能搶到,就會(huì)從 Blocked 狀態(tài)回到 Runnable 狀態(tài)。

1.4、wait/notify/notifyAll注意事項(xiàng)

1.4.1、為什么 wait 、notify、notifyAll必須在 synchronized 保護(hù)的同步代碼中使用?

預(yù)防饑餓線程:在以下消費(fèi)者-生產(chǎn)者模式代碼中,生產(chǎn)者執(zhí)行g(shù)ive方法,消費(fèi)者執(zhí)行take方法。如果執(zhí)行順序如下所示,且沒(méi)有其他生產(chǎn)者,那么可能導(dǎo)致消費(fèi)者線程長(zhǎng)久處于wait()狀態(tài)。

[圖片上傳失敗...(image-ea3ef8-1689324444603)]

1.4.2、為什么 wait/notify/notifyAll 被定義在 Object 類中,而 sleep 定義在 Thread 類中?

每個(gè)對(duì)象都有一個(gè)monitor鎖,鎖是對(duì)象級(jí)別的,而非線程級(jí)別的操作。wait、notify、notifyAll都是鎖級(jí)別的操作,所以將這三個(gè)方法定義在Object類中(Object是所有對(duì)象的父類)

而sleep的作用是休眠線程,是線程級(jí)別的,不會(huì)操作對(duì)象鎖,所以直接放在Thread類中。

1.4.3、wait/notify 和 sleep 方法的異同?

相同點(diǎn):

都會(huì)阻塞線程、都可以響應(yīng)interrupt中斷--->拋出異常
不同點(diǎn):

  1. wait/notify必須放在synchronized中,sleep不用;
  2. wait阻塞線程會(huì)釋放鎖,sleep不會(huì)釋放鎖;
  3. wait的線程必須等待notify喚醒,而sleep可設(shè)置休眠時(shí)間恢復(fù)自動(dòng)喚醒線程;
  4. wait/notify是Object類的方法,sleep是Thread類方法。

1.5、實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式的方法

1.5.1、BlockingQueue

public static void main(String[] args) {
  BlockingQueue queue = new ArrayBlockingQueue<>(10);
 Runnable producer = () -> {
    while (true) {
          queue.put(new Object());
  }
 };
new Thread(producer).start();
new Thread(producer).start();

Runnable consumer = () -> {
      while (true) {
           queue.take();
    }
};
new Thread(consumer).start();
new Thread(consumer).start();
}

1.5.2、Condition

public class MyBlockingQueueForCondition {
   private Queue queue;
   private int max = 16;
   private ReentrantLock lock = new ReentrantLock();
   private Condition notEmpty = lock.newCondition();
   private Condition notFull = lock.newCondition();

   public MyBlockingQueueForCondition(int size) {
       this.max = size;
       queue = new LinkedList();
   }
   public void put(Object o) throws InterruptedException {
       lock.lock();
       try {
           while (queue.size() == max) {
               notFull.await();
           }
           queue.add(o);
           notEmpty.signalAll();
       } finally {
           lock.unlock();
       }
   }

   public Object take() throws InterruptedException {
       lock.lock();
       try {
           while (queue.size() == 0) {
               notEmpty.await();
           }
           Object item = queue.remove();
           notFull.signalAll();
           return item;
       } finally {
           lock.unlock();
       }
   }
}

1.5.3、 wait/notify

class MyBlockingQueue {
   private int maxSize;
   private LinkedList storage;

   public MyBlockingQueue(int size) {
       this.maxSize = size;
       storage = new LinkedList<>();
   }

   public synchronized void put() throws InterruptedException {
       while (storage.size() == maxSize) {
           wait();
       }
       storage.add(new Object());
       notifyAll();
   }

   public synchronized void take() throws InterruptedException {
       while (storage.size() == 0) {
           wait();
       }
       System.out.println(storage.remove());
       notifyAll();
   }
}

2、線程安全

2.1、3種線程安全問(wèn)題

2.1.1、運(yùn)行結(jié)果問(wèn)題

多線程情況下,多次執(zhí)行同一段代碼,運(yùn)行結(jié)果不同。

2.1.2、發(fā)布和初始化導(dǎo)致線程安全問(wèn)題

新建一個(gè)線程用于對(duì)象初始化,在初始化操作完成之前,另一個(gè)線程使用該對(duì)象,對(duì)象的屬性可能就不是預(yù)期的初始化后的值。

2.1.3、活躍性問(wèn)題

  • 死鎖:兩個(gè)線程互相持有對(duì)方需要的資源(鎖),但又互不相讓。
  • 活鎖:線程未阻塞,但線程運(yùn)行異常,報(bào)錯(cuò)時(shí)無(wú)限重試。導(dǎo)致線程處于活躍狀態(tài)卻永遠(yuǎn)得不到結(jié)果。
  • 饑餓:一個(gè)線程長(zhǎng)時(shí)間在等待資源:比如1個(gè)低優(yōu)先級(jí)的線程,和1000000個(gè)高優(yōu)先級(jí)線程(不斷生產(chǎn)),此時(shí)這個(gè)低優(yōu)先級(jí)線程就可能沒(méi)機(jī)會(huì)運(yùn)行,導(dǎo)致饑餓。

2.2、哪些場(chǎng)景需要注意線程安全問(wèn)題

  • 訪問(wèn)共享變量或資源
  • 多線程下依賴時(shí)序的操作
if (map.containsKey(key)) {
    map.remove(obj)
}
  • 對(duì)方?jīng)]有聲明自己是線程安全的(比如ArrayList)

2.3、為什么多線程會(huì)帶來(lái)性能問(wèn)題

  • 上下文切換:上下文切換會(huì)掛起當(dāng)前正在執(zhí)行的線程并保存當(dāng)前的狀態(tài),然后尋找下一處即將恢復(fù)執(zhí)行的代碼,喚醒下一個(gè)線程,以此類推,反復(fù)執(zhí)行
  • 緩存失效:線程切換后,CPU高速緩存會(huì)失效
  • 多線程間協(xié)作開(kāi)銷大

3、線程池

3.1、線程池的好處

  • 池中有固定線程,復(fù)用線程,避免頻繁創(chuàng)建銷毀線程,減少線程生命周期的開(kāi)銷,提高性能
  • 根據(jù)配置和任務(wù)數(shù)量控制線程數(shù)量,避免線程過(guò)多導(dǎo)致資源浪費(fèi)
  • 統(tǒng)一管理池中的線程

3.2、創(chuàng)建線程池各參數(shù)的含義

[圖片上傳失敗...(image-5ce32d-1689324444603)]

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue

->

new ThreadPoolExecutor(核心線程數(shù),最大線程數(shù),非核心線程存活時(shí)間,時(shí)間單位,緩沖隊(duì)列,創(chuàng)建線程使用的工廠,任務(wù)拒絕策略)。

3.3、線程池執(zhí)行流程

new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit unit, BlockingQueue

核心線程數(shù)->緩沖隊(duì)列->最大線程數(shù)->拒絕策略。

[圖片上傳失敗...(image-1cae81-1689324444603)]

流程:

  1. 核心線程數(shù)未滿時(shí),每次來(lái)任務(wù)就會(huì)創(chuàng)建新的核心線程來(lái)執(zhí)行任務(wù);
  2. 核心線程滿了,隊(duì)列未滿,則將任務(wù)放到隊(duì)列中;
  3. 核心線程滿了,隊(duì)列滿了,線程未達(dá)到最大線程數(shù),創(chuàng)建非核心線程數(shù)執(zhí)行線程;
  4. 核心線程滿了,隊(duì)列滿了,線程已達(dá)到最大線程數(shù),此時(shí)線程池工作飽和,執(zhí)行線程池的拒絕策略。

3.4、默認(rèn)四種拒絕策略

3.4.1、拒絕時(shí)機(jī)

  1. 調(diào)用shutdown等方法關(guān)閉線程池后,正在執(zhí)行線程池內(nèi)剩余的線程,此時(shí)再向線程池提交任務(wù),就會(huì)遭到拒絕。
  2. 線程池已經(jīng)沒(méi)有能力處理新的任務(wù):隊(duì)列已滿、線程池已滿。

3.4.2、拒絕策略

  • AbortPolicy:直接拋出異常
  • DiscardPolicy:直接丟棄新來(lái)的任務(wù),用戶無(wú)法感知
  • DiscardOldestPolicy:丟棄隊(duì)列中存活時(shí)間最長(zhǎng)的任務(wù),用戶無(wú)法感知
  • CallerRunsPolicy:把新來(lái)的任務(wù)直接交個(gè)調(diào)用者線程運(yùn)行

3.4、常見(jiàn)線程池

FixedThreadPool:固定線程池:核心線程數(shù) = 最大線程數(shù)
CachedThreadPool:可緩存線程池:線程數(shù)可以無(wú)限增加,并可對(duì)閑置線程進(jìn)行回收
ScheduledThreadPool:定時(shí)/周期性線程池:支持定時(shí)或周期性執(zhí)行任務(wù)(多數(shù)情況下可用Timer類代替)
SingleThreadExecutor:?jiǎn)尉€程線程池

3.5、ForkJoinPool

3.5.1、功能

將一個(gè)大任務(wù)拆分成多個(gè)小任務(wù)后,使用fork可以將小任務(wù)分發(fā)給其他線程同時(shí)處理,使用join可以將多個(gè)線程處理的結(jié)果進(jìn)行匯總

3.5.2、父類

RecursiveAction:用于沒(méi)有返回結(jié)果的任務(wù)(類似Runnable)
RecursiveTask:用于有返回結(jié)果的任務(wù)(類似Callable)

3.5.3、自定義Task類

class CountTask extends RecursiveTask {
    private static final int THREADHOLD = 2;
    private int start;
    private int end;
    /**
     * @Description: 最小的一個(gè)任務(wù)單元,以及它要處理的范圍
     * @param:
     * @return:
     */
    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        boolean canCompute = (end - start)

3.5.4、創(chuàng)建線程池

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinPool forkJoinPool = new ForkJoinPool(2);
        CountTask sumTask1 = new CountTask(0, 4);

        ForkJoinTask task1 = forkJoinPool.submit(sumTask1);
        System.out.println(task1.get());
    }

3.5.5、任務(wù)運(yùn)行原理

[圖片上傳失敗...(image-b4e29f-1689324444603)]

3.6、線程池常用的阻塞隊(duì)列

LinkedBlockingQueue:無(wú)界隊(duì)列
核心線程數(shù)固定的線程池,需要一個(gè)沒(méi)有容量限制的阻塞隊(duì)列。
FixedThreadPool、SingleThreadExector
SynchronousQueue
容量為0,一旦有任務(wù)進(jìn)入隊(duì)列,就立刻將任務(wù)提交給線程池,所以需要線程池有足夠的線程數(shù)
CachedThreadPool
DelayedWorkQueue
該隊(duì)列把任務(wù)按照延時(shí)時(shí)間長(zhǎng)短進(jìn)行排序,方便任務(wù)進(jìn)行
ScheduledThreadPool

3.6、為什么不要通過(guò)Executors創(chuàng)建線程池

  • 內(nèi)存溢出:newFixedThreadPool、newSingleThreadExecutor
  • OOM:newCachedThreadPool
  • 通過(guò)使用new ThreadPoolExecutor()手動(dòng)創(chuàng)建線程池,根據(jù)實(shí)際情況選擇合適的隊(duì)列、拒絕策略、線程數(shù)等,避免資源耗盡的風(fēng)險(xiǎn)。

3.7、合適的線程數(shù)量

目的:合適的線程數(shù)量可以更好地使用CPU和內(nèi)存等資源,從而最大限度地提升程序的性能。

3.7.1、CPU密集型任務(wù):

  • 線程數(shù) = CPU核心數(shù)的1~2倍
  • 線程數(shù) = CPU 核心數(shù) *(1+平均等待時(shí)間/平均工作時(shí)間)

計(jì)算任務(wù)重,單個(gè)任務(wù)耗時(shí)長(zhǎng),如果線程數(shù)過(guò)多,會(huì)造成不必要的上下文切換,降低系統(tǒng)性能。

3.7.2、耗時(shí)IO型任務(wù):

線程數(shù) = CPU 核心數(shù) *(1+平均等待時(shí)間/平均工作時(shí)間)

IO 讀寫速度相比于 CPU 的速度而言是比較慢的,如果我們?cè)O(shè)置過(guò)少的線程數(shù),就可能導(dǎo)致 CPU 資源的浪費(fèi)。而如果我們?cè)O(shè)置更多的線程數(shù),那么當(dāng)一部分線程正在等待 IO 的時(shí)候,它們此時(shí)并不需要 CPU 來(lái)計(jì)算,那么另外的線程便可以利用 CPU 去執(zhí)行其他的任務(wù),互不影響,這樣的話在任務(wù)隊(duì)列中等待的任務(wù)就會(huì)減少,可以更好地利用資源。

3.8、如何正確關(guān)閉線程池

  • *shutdown()-推薦

拒絕新任務(wù)的提交,執(zhí)行完當(dāng)前線程池中正在執(zhí)行的任務(wù)及隊(duì)列中的任務(wù)后,關(guān)閉線程池。

  • isShutdown()

判斷線程池是否已經(jīng)開(kāi)始了關(guān)閉工作;
當(dāng)調(diào)用shutdown()或shutdownNow()方法后返回為true;
為true僅表示關(guān)閉流程開(kāi)始,不一定已經(jīng)關(guān)閉。

  • isTerminated()

線程池已關(guān)閉,且池中和隊(duì)列中所有線程已執(zhí)行完畢時(shí),返回true

  • awaitTermination()

線程等待一段指定的時(shí)間,如果在等待時(shí)間內(nèi),線程池已關(guān)閉并且內(nèi)部的任務(wù)都執(zhí)行完畢了,那么方法就返回 true,否則超時(shí)返回 fasle。

  • *shutdownNow()-推薦

給所有線程發(fā)送中斷信號(hào),然后會(huì)將隊(duì)列中正在等待的所有任務(wù)轉(zhuǎn)移到一個(gè) List 中并返回,我們可以根據(jù)返回的任務(wù) List 來(lái)進(jìn)行一些補(bǔ)救的操作。

3.9、如何設(shè)計(jì)線程池

  • 考慮線程池中的個(gè)數(shù)是固定的還是動(dòng)態(tài)變化的。
  • 考慮隊(duì)列大小:無(wú)界隊(duì)列(可能導(dǎo)致內(nèi)存耗盡);有界隊(duì)列(隊(duì)列滿了拒絕策略)
  • 每次提交新任務(wù),是直接放入隊(duì)列,還是開(kāi)啟新線程
  • 沒(méi)有任務(wù)時(shí),線程是睡眠一段時(shí)間還是進(jìn)入阻塞

4、鎖

4.1、鎖分類

4.1.1、偏向鎖/輕量級(jí)鎖/重量級(jí)鎖

特指synchronized鎖的狀態(tài)。在對(duì)象頭中的mark word來(lái)表明鎖的狀態(tài)。

偏向鎖:不加鎖,加標(biāo)記。

輕量級(jí)鎖:不阻塞,通過(guò)自旋和CAS獲取鎖。

重量級(jí)鎖:阻塞,互斥鎖。

鎖升級(jí):無(wú)鎖→偏向鎖→輕量級(jí)鎖→重量級(jí)鎖

4.1.2、可重入鎖/非可重復(fù)鎖

已經(jīng)持有鎖,是否能在不釋放當(dāng)前鎖的情況下,再次獲取當(dāng)前鎖。

4.1.3、共享鎖/獨(dú)占鎖

是否可被多個(gè)線程同時(shí)獲取鎖。典型為讀寫鎖。

4.1.4、公平鎖/非公平鎖

線程是否按照先來(lái)先到的原則獲取鎖。

4.1.5、悲觀鎖/樂(lè)觀鎖

悲觀鎖:獲取資源前,必須先獲得鎖,鎖住資源,synchronized。
樂(lè)觀鎖:獲取資源前,不用獲得鎖,不會(huì)鎖住資源,CAS。

適用場(chǎng)景:
悲觀鎖:并發(fā)寫入多、臨界區(qū)代碼復(fù)雜、競(jìng)爭(zhēng)激烈等場(chǎng)景。
樂(lè)觀鎖:大部分是讀取,少部分是修改的場(chǎng)景,也適合雖然讀寫都很多,但是并發(fā)并不激烈的場(chǎng)景。

4.1.6、自旋鎖/非自旋鎖

是否利用循環(huán),不停地嘗試獲取鎖。

4.1.7、可中斷鎖/不可中斷鎖

可中斷鎖:一旦申請(qǐng)鎖,就只能拿到鎖后才能做其他操作(synchronized)。
不可中斷鎖:線程申請(qǐng)鎖后,可中斷獲取鎖的流程(ReentrantLock)。

4.2、synchronized的monitor鎖

javac xxx.java:產(chǎn)生xxx.class
javap -verbose xxx.class:生成class文件的反匯編內(nèi)容

4.2.1、同步代碼塊

monitorenter(加鎖)、monitorexit(釋放鎖)指令:每個(gè)對(duì)象頭有一個(gè)記錄著鎖次數(shù)的計(jì)數(shù)器

4.2.2、同步方法

ACC_SYNCHRONIZED標(biāo)識(shí)符:如果方法有該標(biāo)識(shí)符,那么在調(diào)用方法時(shí)會(huì)先判斷獲取對(duì)象的monitor鎖,方法執(zhí)行之后釋放monitor鎖。

4.3、如何選擇synchronized和Lock

4.3.1、相同點(diǎn)

  • 保證線程安全;
  • 保證可見(jiàn)性(前一個(gè)加鎖操作結(jié)果對(duì)后一個(gè)加鎖操作可見(jiàn));
  • 可重入鎖。

4.3.2、不同點(diǎn)

synchronized

  1. 自動(dòng)加鎖和釋放鎖。
  2. 加解鎖順序:加鎖Lock1->加鎖Lock2->解鎖Lock2->解鎖Lock1。
  3. 申請(qǐng)鎖后,只有獲得鎖后才能繼續(xù)其他操作,否則會(huì)永遠(yuǎn)等待。
  4. 只能被一個(gè)線程擁有。
  5. 不可設(shè)置公平鎖/非公平鎖。
  6. Java5之前,性能低;Java6之后,增加自旋鎖、輕量級(jí)鎖、偏向鎖,性能提高

Lock

  1. 必須顯示lock()和unlock()
  2. 加解鎖順序:可自由控制加解鎖順序
  3. 申請(qǐng)鎖后,可中斷退出。
  4. 可被多個(gè)線程擁有。
  5. 可設(shè)置公平鎖/非公平鎖。

4.3.3、如何選擇

  1. 最好的情況是兩者都不用,優(yōu)先使用并發(fā)工具包來(lái)加解鎖;
  2. 盡量使用synchronized由于lock,因?yàn)閟ynchronized可自動(dòng)加解鎖;
  3. 需要使用到嘗試獲取鎖、鎖中斷、超時(shí)功能時(shí),才使用Lock。

4.4、Lock的常用方法(ReentrantLock)

  • lock()

獲取鎖,不能被中斷,死鎖->永久等待。必須在finally中unlock解鎖。

  • tryLock()

嘗試獲取鎖,獲取成功,返回true;否則返回false。可根據(jù)是否獲得鎖決定后續(xù)程序的行為。

  • tryLock(long time, TimeUnit unit)

與tryLock類似,區(qū)別在于獲取鎖失敗后會(huì)等待一段時(shí)間,一段時(shí)間后還獲取不到鎖,再返回false。

  • lockInterruptibly()

獲取鎖,能獲取則直接返回,不能獲取則一直嘗試獲取鎖,可響應(yīng)中斷。

  • unlock()

釋放鎖,必須寫在finally塊中。

4.5、公平鎖和非公平鎖

  • 公平鎖:按照線程請(qǐng)求順序分配鎖。慢,吞吐量小,喚醒阻塞線程開(kāi)銷大。
  • 非公平鎖:在一定情況下,允許插隊(duì),但不是隨意插隊(duì)??欤掏铝看?,有可能產(chǎn)生饑餓線程。
  • 通過(guò)new ReentrantLock(false) 來(lái)設(shè)置公平鎖/非公平鎖。

4.5.1、為什么有非公平鎖

示例:A,B,C三個(gè)線程,A持有鎖,B阻塞等待獲取鎖
A釋放鎖時(shí),剛好C來(lái)獲取鎖,非公平鎖就可把鎖給C。
(B處于阻塞狀態(tài),喚醒B需要很大開(kāi)銷)
新線程都會(huì)先獲取鎖,獲取不到,再講線程放到隊(duì)列尾,之后按順序喚醒線程獲取鎖。

4.6、讀寫鎖ReadWriteLock

  • 讀讀共享,讀寫互斥、寫寫互斥。讀鎖可以被多個(gè)線程獲取,而寫鎖不會(huì)。
  • 讀寫鎖適用于讀多寫少的場(chǎng)景。

4.7、讀寫鎖升降級(jí)

4.7.1、非公平鎖下的讀寫操作

  • 寫鎖允許插隊(duì):在沒(méi)有其他線程持有讀鎖、寫鎖的時(shí)候,能插隊(duì)成功;插隊(duì)失敗進(jìn)行等待隊(duì)列。
  • 讀鎖不允許插隊(duì)(防止饑餓線程)

4.7.2、讀寫鎖的降級(jí)

在不釋放寫鎖的情況下,直接獲取讀鎖,然后再釋放寫鎖。(區(qū)別于直接釋放寫鎖之后再獲取讀鎖---中間有時(shí)間差,其他線程可能獲得鎖)
長(zhǎng)時(shí)間持有寫鎖,浪費(fèi)資源,此時(shí)會(huì)禁止其他線程的讀取。

4.7.3、不支持讀寫鎖的升級(jí):讀鎖->寫鎖。

場(chǎng)景:升級(jí)時(shí),需要等待所有讀鎖都釋放。
示例:A、B兩個(gè)線程持有讀鎖,且都想升級(jí)為寫鎖,且A、B都在等待對(duì)方釋放讀鎖,此時(shí)就會(huì)發(fā)生死鎖現(xiàn)象。

4.7.4、鎖降級(jí)中讀鎖的獲取是否必要呢?

答案是必要的。主要是為了保證數(shù)據(jù)的可見(jiàn)性,如果當(dāng)前線程不獲取讀鎖而是直接釋放寫鎖, 假設(shè)此刻另一個(gè)線程(記作線程T)獲取了寫鎖并修改了數(shù)據(jù),那么當(dāng)前線程無(wú)法感知線程T的數(shù)據(jù)更新。如果當(dāng)前線程獲取讀鎖,即遵循鎖降級(jí)的步驟,則線程T將會(huì)被阻塞,直到當(dāng)前線程使用數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫鎖進(jìn)行數(shù)據(jù)更新。

4.8、自旋鎖

在java1.5及以上的并發(fā)包中,AtomicXXX等原子類基本都是自旋鎖的實(shí)現(xiàn)。

定義

  • 自旋鎖:不會(huì)放棄CPU時(shí)間片,不停地嘗試獲取鎖,直到獲取成功為止。
  • 非自旋鎖:獲取鎖失敗后,線程休眠、釋放CPU資源。

好處
阻塞和喚醒線程需要高昂開(kāi)銷,自旋鎖可避免上下文切換,節(jié)省線程狀態(tài)切換帶來(lái)的開(kāi)銷。
缺點(diǎn)
如果鎖一直被其他線程持有,那么就會(huì)一直自旋,白白浪費(fèi)處理器資源。

4.9、JVM對(duì)synchronized的優(yōu)化

自旋、鎖消除、鎖粗化、偏向鎖、輕量級(jí)鎖

4.9.1、自旋

自適應(yīng)的自旋鎖:會(huì)根據(jù)最近自旋嘗試的成功率、失敗率等因素來(lái)決定自旋時(shí)間,以解決自旋鎖長(zhǎng)時(shí)間進(jìn)行無(wú)用自旋的問(wèn)題。

4.9.2、鎖消除

如果某些對(duì)象不可能被其他對(duì)象訪問(wèn)到,就可以把他們當(dāng)成棧上數(shù)據(jù),只能被當(dāng)前線程訪問(wèn),絕對(duì)線程安全。此時(shí),編譯器就會(huì)把synchronized給消除,省去加鎖和解鎖的開(kāi)銷。

4.9.3、鎖粗化

舉例說(shuō)明:
有三個(gè)連續(xù)的同步代碼塊,每執(zhí)行一個(gè)同步代碼塊時(shí),都要進(jìn)行加鎖和解鎖操作。因?yàn)檫B續(xù),此時(shí)可以把三個(gè)同步代碼塊,合并為一個(gè)同步代碼塊,增大了同步區(qū)域,即為鎖粗化。
減少了中間無(wú)意義的加鎖和解鎖過(guò)程。
如果循環(huán)場(chǎng)景使用鎖粗化功能,會(huì)導(dǎo)致其他線程長(zhǎng)時(shí)間無(wú)法獲得鎖,所以鎖粗化功能僅適用于非循環(huán)的場(chǎng)景。

4.9.4、鎖升級(jí)策略

無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖

4.10、鎖升級(jí)策略

  • 鎖升級(jí):無(wú)鎖->偏向鎖->輕量級(jí)鎖->重量級(jí)鎖
  • Java對(duì)象頭 = Mark Word + 指向類的指針 + 數(shù)組長(zhǎng)度(只有數(shù)組對(duì)象才有)
  • Mark Word記錄對(duì)象和鎖有關(guān)的信息,根據(jù)鎖標(biāo)志位來(lái)確定當(dāng)前對(duì)象是什么鎖狀態(tài)。

[圖片上傳失敗...(image-b132e8-1689324444603)]

4.10.1、無(wú)鎖

鎖標(biāo)志位 = 01,是否偏向鎖 = 0,存儲(chǔ)對(duì)象的hashcode值。

當(dāng)對(duì)象沒(méi)有被當(dāng)成鎖時(shí),Mark Word記錄的是對(duì)象的hashcode。

4.10.2、 無(wú)鎖-> 偏向鎖

鎖標(biāo)志位 = 01,是否偏向鎖 = 1,存儲(chǔ)占有偏向鎖線程的線程id。

當(dāng)對(duì)象被當(dāng)成同步鎖并且線程A通過(guò)CAS搶到鎖時(shí),進(jìn)入偏向鎖狀態(tài),將線程A的線程id寫入Mark Word。

當(dāng)線程A再次試圖獲取鎖時(shí),JVM發(fā)現(xiàn)同步鎖為偏向鎖,且偏向鎖中的線程id值等于線程A的id,就可以直接執(zhí)行同步鎖的代碼塊;

當(dāng)線程B來(lái)試圖獲取鎖時(shí),JVM發(fā)現(xiàn)同步鎖為偏向鎖,且偏向鎖中的線程id值不等于線程B的id,線程B就會(huì)通過(guò)CAS去嘗試獲取鎖:如果獲取成功,則將Mark Word中的偏向鎖的線程id修改為線程B的id;如果獲取失敗,則將同步鎖升級(jí)為輕量級(jí)鎖;

4.10.3、 偏向鎖升級(jí)為輕量級(jí)鎖

出現(xiàn)鎖競(jìng)爭(zhēng)(CAS獲取鎖失敗) 時(shí),偏向鎖升級(jí)為輕量級(jí)鎖。

鎖標(biāo)志位 = 00,存儲(chǔ)指向棧中鎖記錄的指針。

JVM在當(dāng)前線程的棧幀中開(kāi)辟一塊獨(dú)立空間,用以保存指向?qū)ο箧iMark Word的指針,同時(shí)Mark Word保存指向棧幀中獨(dú)立空間的指針。

如果保存成功,則當(dāng)前線程獲取該對(duì)象的輕量級(jí)鎖,執(zhí)行同步代碼;

如果保存失敗,則當(dāng)前線程 自旋,不斷嘗試獲取鎖(嘗試N次,由JVM決定),如果搶鎖成功,執(zhí)行同步代碼;如果搶鎖失敗,則將輕量級(jí)鎖升級(jí)為重量級(jí)鎖。

4.10.4、 輕量級(jí)鎖-> 重量級(jí)鎖

輕量級(jí)鎖自旋超過(guò)JVM指定次數(shù)時(shí),升級(jí)為重量級(jí)鎖。

鎖標(biāo)志位 = 10,存儲(chǔ)指向重量級(jí)鎖的指針。

未搶占到鎖的線程會(huì)被阻塞,搶到鎖的線程在釋放鎖的同時(shí)會(huì)喚醒那些阻塞的線程。

重量級(jí)鎖下,線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài),成本較高。

4.10.5、適用場(chǎng)景

  • 偏向鎖:只有一個(gè)線程在臨界區(qū)執(zhí)行,無(wú)需操作系統(tǒng)介入;
  • 輕量級(jí)鎖:多個(gè)線程交替進(jìn)入臨界區(qū),競(jìng)爭(zhēng)不激烈,就算有沖突,線程自旋幾次就能獲取鎖;
  • 重量級(jí)鎖:多個(gè)線程出現(xiàn)激烈競(jìng)爭(zhēng)。

4.10.6、鎖的優(yōu)缺點(diǎn)比較

[圖片上傳失敗...(image-6a3c1e-1689324444603)]

5、并發(fā)容器

5.1、HashMap為什么線程不安全

  • *擴(kuò)容期間取出的值不準(zhǔn)確

擴(kuò)容時(shí)會(huì)創(chuàng)建一個(gè)新的Entry數(shù)組,然后遍歷原Entry數(shù)組,把所有的Entry重新Hash到新數(shù)組。
在這個(gè)過(guò)程中另一個(gè)線程從HashMap取數(shù)據(jù),就有可能取出null。

  • *循環(huán)鏈表(死循環(huán))

原因:并發(fā)+頭插
Jdk1.7在高并發(fā)下,HashMap產(chǎn)生死循環(huán),造成CPU100%負(fù)載:HashMap在存儲(chǔ)時(shí),若size超過(guò)當(dāng)前最大容量*負(fù)載因子時(shí),會(huì)增加桶的數(shù)目,進(jìn)行HashMap數(shù)組擴(kuò)容(resize()),在resize過(guò)程中,會(huì)調(diào)用transfer()方法將鏈表轉(zhuǎn)換成新鏈表,在多線程情況下可能導(dǎo)致鏈表回路,從而導(dǎo)致死循環(huán)。
擴(kuò)容時(shí),會(huì)調(diào)用transfer方法將舊的元素重新hash后放到新的table中。

Jdk1.8中HashMap的put元素操作由1.7的頭插改為尾插,避免了死循環(huán)問(wèn)題。

5.2、ConcurrentHashMap

  • *JDK1.7

Segment鎖分段技術(shù)
segment數(shù)組(存儲(chǔ)鎖)+hashEntry數(shù)組
采用鎖分段技術(shù)來(lái)保證線程安全,將數(shù)據(jù)分為一段一段的,給每段數(shù)據(jù)各分配一把鎖,不同數(shù)據(jù)段之間的讀寫互不影響,效率高
最大并發(fā)個(gè)數(shù)就是 Segment 的個(gè)數(shù),默認(rèn)是 16

  • *JDK1.8

Node數(shù)組+鏈表/紅黑樹(shù)
并發(fā)控制使用 synchronized 和 CAS 來(lái)操作
synchronized只鎖定當(dāng)前鏈表或紅黑二叉樹(shù)的首節(jié)點(diǎn),這樣只要hash不沖突,就不會(huì)產(chǎn)生并發(fā),效率又提升N倍

5.3、為什么Map桶中超過(guò)8個(gè)才轉(zhuǎn)為紅黑樹(shù)

  • 鏈表長(zhǎng)度>=8,且容量>=MIN_TREEIFY_CAPACITY(默認(rèn)為64)時(shí),鏈表轉(zhuǎn)為紅黑樹(shù)
  • 鏈表長(zhǎng)度

單個(gè)TreeNode需要占用的空間大約是普通Node的兩倍。所以使用紅黑樹(shù),就是用空間換時(shí)間。

泊松分布:
在理想情況下,鏈表長(zhǎng)度符合泊松分布,各個(gè)長(zhǎng)度的命中概率依次遞減,當(dāng)長(zhǎng)度為 8 的時(shí)候,概率僅為 0.00000006。這是一個(gè)小于千萬(wàn)分之一的概率

5.4、ConcurrentHashMap 和 Hashtable 的區(qū)別

  • *Hashtable

synchronized -鎖整個(gè)Map
不允許在迭代期間修改內(nèi)容

  • *ConcurrentHashMap

CAS + synchronized + Node-鎖一個(gè)Node
允許在迭代期間修改內(nèi)容

5.5、CopyOnWriteArrayList

5.5.1、適用場(chǎng)景

  • 對(duì)讀操作性能有要求,對(duì)寫操作性能沒(méi)要求;
  • 讀多寫少。

5.5.2、特點(diǎn)

寫入不會(huì)阻塞讀取操作,可以在寫入的同時(shí)讀取。打破了讀寫鎖讀寫互斥的限制

5.5.3、原理

  1. 在寫入操作時(shí),先復(fù)制一份容器的副本;
  2. 在副本中進(jìn)行寫操作,并行的讀操作依然在原容器中進(jìn)行;
  3. 修改完成后,將原容器的引用指向副本容器。

5.5.4、缺陷

  1. 占用內(nèi)存,在寫操作時(shí),會(huì)同時(shí)存在兩個(gè)對(duì)象占用內(nèi)存;
  2. 容器元素較多時(shí),復(fù)制開(kāi)銷大;
  3. 寫入操作會(huì)先寫入副本,對(duì)其他線程來(lái)說(shuō),數(shù)據(jù)不能保證可見(jiàn)性。

6、阻塞隊(duì)列(BlockingQueue )

6.1、阻塞隊(duì)列

特點(diǎn):線程安全。
阻塞功能使得生產(chǎn)者和消費(fèi)者兩端的能力得以平衡,當(dāng)有任何一端速度過(guò)快時(shí),阻塞隊(duì)列便會(huì)把過(guò)快的速度給降下來(lái)。(take()、put())

適用場(chǎng)景:常用于生產(chǎn)者-消費(fèi)者模式

常用實(shí)現(xiàn)類
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue、PriorityBlockingQueue 和 LinkedTransferQueue

6.2、常用方法

拋出異常
add:往隊(duì)列里添加一個(gè)元素,如果隊(duì)列滿了,拋出異常;
remove:刪除元素,如果隊(duì)列為空,拋出異常;
element:返回隊(duì)列的頭部節(jié)點(diǎn),但不刪除,如果隊(duì)列為空,拋出異常。
返回結(jié)果但不拋出異常
offer:插入一個(gè)元素,插入成功返回true,插入失敗(隊(duì)列已滿)返回false;
poll:移除并返回隊(duì)列頭節(jié)點(diǎn),隊(duì)列為空,則返回null;
peek:返回隊(duì)列的頭節(jié)點(diǎn)但不刪除,隊(duì)列為空,則返回null。
阻塞
put:插入一個(gè)元素,如果隊(duì)列滿了,則阻塞;
take:獲取并移除隊(duì)列頭節(jié)點(diǎn),如果隊(duì)列為空,則阻塞。

6.3、常見(jiàn)的阻塞隊(duì)列

  • *ArrayBlockingQueue

底層為數(shù)組,有界隊(duì)列,利用ReentrantLock實(shí)現(xiàn)線程安全。可設(shè)置容量和是否公平。

  • *LinkedBlockingQueue

底層為鏈表,無(wú)界隊(duì)列。

  • *SynchronousQueue

該隊(duì)列容量為0,內(nèi)部不存放任何數(shù)據(jù)嗎,數(shù)據(jù)直接傳遞;
每次放數(shù)據(jù)都會(huì)阻塞,直到有消費(fèi)者來(lái)取數(shù)據(jù);
每次取數(shù)據(jù)都會(huì)阻塞,直到有生產(chǎn)者來(lái)存放數(shù)據(jù)。

  • *PriorityBlockingQueue

自定義排序(元素必須支持比較大小),支持優(yōu)先級(jí)的無(wú)界阻塞隊(duì)列。
自定義類實(shí)現(xiàn)compareTo()方法指定元素排序規(guī)則;
初始化算構(gòu)造器參數(shù)comparator指定排序規(guī)則。

  • *DelayQueue

具有延遲功能的無(wú)界隊(duì)列;
根據(jù)延遲時(shí)間的長(zhǎng)度,決定元素在隊(duì)列中的位置;
越靠近隊(duì)列頭的代表越早過(guò)期。

6.4、并發(fā)安全原理

  • *阻塞隊(duì)列(ArrayBoockingQueue為例)
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;

讀操作和寫操作都需要獲得lock獨(dú)占鎖才能進(jìn)行下一步操作;
讀操作時(shí)如果隊(duì)列為空,線程就會(huì)進(jìn)入到讀線程專屬的 notEmpty 的 Condition 的隊(duì)列中去排隊(duì),等待寫線程寫入新的元素;
寫操作時(shí)如果隊(duì)列已滿,此時(shí)寫操作的線程會(huì)進(jìn)入到寫線程專屬的 notFull 隊(duì)列中去排隊(duì),等待讀線程將隊(duì)列元素移除并騰出空間。

  • *非阻塞隊(duì)列(ConcurrentLinkedQueue為例)

死循環(huán) + 樂(lè)觀鎖

6.5、如何選擇阻塞隊(duì)列

6.5.1、線程池對(duì)應(yīng)的阻塞隊(duì)列

根據(jù)線程池的容量和隊(duì)列的容量來(lái)選擇

[圖片上傳失敗...(image-304f8d-1689324444603)]

6.5.2、如何選擇

  1. 功能:排序、延遲等;
  2. 容量:容量固定、容量無(wú)限、容量為0;
  3. 能否擴(kuò)容:考慮是否需要?jiǎng)討B(tài)擴(kuò)容;
  4. 內(nèi)存結(jié)構(gòu):數(shù)組、鏈表;
  5. 性能:SynchronousQueue(直接傳遞,無(wú)需保存,性能高);LinkedBlockingQueue(兩把鎖)、ArrayBlockingQueue(一把鎖),并發(fā)程度高時(shí),LinkedBlockingQueue性能更好。

7、原子類

7.1、原子類如何利用CAS保證線程安全

7.1.1、原理

循環(huán) + CAS; Unsafe 類是 CAS 的核心類。

CAS操作包括了3個(gè)操作數(shù)(多個(gè)操作,原子性由硬件層面保證):
? 需要讀寫的內(nèi)存位置(V)
? 進(jìn)行比較的預(yù)期值(A)
? 擬寫入的新值(B)
CAS操作邏輯如下:如果內(nèi)存位置V的值等于預(yù)期的A值,則將該位置更新為新值B,否則不進(jìn)行任何操作。許多CAS的操作是自旋的:如果操作不成功,會(huì)一直重試,直到操作成功為止。

Unsafe類中的getAndAddInt方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
   int var5;
   do {
       var5 = this.getIntVolatile(var1, var2);
   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
   return var5;
}

7.1.2、原子類

  • Atomic 基本類型原子類

AtomicInteger、AtomicLong、AtomicBoolean

以AtomicInteger為例:
public final int get() //獲取當(dāng)前的值
public final int getAndIncrement() //獲取當(dāng)前的值,并自增
public final int getAndDecrement() //獲取當(dāng)前的值,并自減
public final int getAndAdd(int delta) //獲取當(dāng)前的值,并加上預(yù)期的值
boolean compareAndSet(int expect, int update) //如果輸入的數(shù)值等于預(yù)期值,則以原子方式將該值更新為輸入值(update)
  • AtomicArray 數(shù)組類型原子

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

數(shù)組里的元素,都可以保證原子性

  • AtomicReference 引用類型原子

AtomicReference、AtomicStampedReference、AtomicMarkableReference

讓一個(gè)對(duì)象保證原子性

  • AtomicFieldUpdater 升級(jí)類型原子

AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

把一個(gè)普通變量升級(jí)成原子類。
場(chǎng)景:
1、歷史代碼變量,已經(jīng)被聲明為普通變量并廣泛應(yīng)用,修改成本高,可升級(jí)成原子類保證線程安全;
2、每天大部分時(shí)間,該變量不需要保持原子性,只有在少數(shù)一兩次操作時(shí),需要保證原子性。(原子類比普通變量更耗費(fèi)資源)

[圖片上傳失敗...(image-b8a43d-1689324444603)]

  • *Adder 累加器

LongAdder、DoubleAdder

  • *Accumulator 積累器

LongAccumulator、DoubleAccumulator

7.2、如何解決AtomicInteger在高并發(fā)下的性能問(wèn)題

7.2.1、原因

高并發(fā)下,本地內(nèi)存和共享內(nèi)存間的數(shù)據(jù)同步會(huì)耗費(fèi)大量資源。

[圖片上傳失敗...(image-c0a034-1689324444603)]

7.2.2、解決方案

LongAdder:分段累加
內(nèi)部有兩個(gè)參數(shù)參與計(jì)數(shù):第一個(gè)叫作 base,它是一個(gè)變量,第二個(gè)是 Cell[] ,是一個(gè)數(shù)組。

低并發(fā)時(shí):base變量
可直接將累加結(jié)果修改到base變量上;

高并發(fā)時(shí):Cell數(shù)組
LongAdder 會(huì)通過(guò)計(jì)算出每個(gè)線程的 hash 值來(lái)給線程分配到不同的 Cell 上去,每個(gè) Cell 相當(dāng)于是一個(gè)獨(dú)立的計(jì)數(shù)器,這樣一來(lái)就不會(huì)和其他的計(jì)數(shù)器干擾,Cell 之間并不存在競(jìng)爭(zhēng)關(guān)系,所以在自加的過(guò)程中,就大大減少了剛才的 flush 和 refresh,以及降低了沖突的概率。
最后會(huì)執(zhí)行 LongAdder.sum() ,把各個(gè)線程里的 Cell 累計(jì)求和,并加上 base,形成最終的總和。

7.2.3、適用場(chǎng)景

  • LongAdder:場(chǎng)景僅僅是需要用到加和減操作的話。
  • AtomicLong:需要利用 CAS 比如 compareAndSet 等復(fù)雜操作。

7.3、原子類和volatile

[圖片上傳失敗...(image-f36729-1689324444603)]

  • volatile:保證變量可見(jiàn)性,變量每次修改會(huì)直接寫入共享內(nèi)存。
  • 原子類:保證i++等復(fù)合操作的原子性。
  • synchronized:既能保證原子性,也能保證可見(jiàn)性。

7.4、AtomicInteger 和 synchronized 的異同點(diǎn)?

  • *AtomicInteger:

CAS保證線程安全;
只能修飾一個(gè)對(duì)象;
性能:樂(lè)觀鎖,開(kāi)銷隨著時(shí)間增加,逐步上漲。

  • *synchronized:

monitor 鎖保證線程安全;
可以鎖方法或代碼塊;
性能:悲觀鎖,開(kāi)銷固定。JDK6之后的鎖升級(jí)會(huì)提升性能。

7.5、Java 8 中 Adder 和 Accumulator 有什么區(qū)別?

  • *Adder:

分段鎖(降低沖突)。base(低并發(fā)),Cell[]數(shù)組(高并發(fā))。
在高并發(fā)下 LongAdder 比 AtomicLong 效率更高

  • *Accumulator:

Accumulator 就是一個(gè)更通用版本的 Adder。
LongAdder 的 API 只有對(duì)數(shù)值的加減,而 LongAccumulator 提供了自定義的函數(shù)操作。

8、ThreadLocal

8.1、適用場(chǎng)景

用作保存每個(gè)線程獨(dú)享的對(duì)象,為每個(gè)線程都創(chuàng)建一個(gè)副本。

每個(gè)線程需要獨(dú)立保存信息,以便當(dāng)前線程內(nèi)其他方法使用。類似于線程內(nèi)的全局變量。
省去一步步傳參的步驟。

示例:simpleDateFormat對(duì)象;
1000個(gè)線程,共享一個(gè)simpleDateFormat對(duì)象------不安全。
1000個(gè)線程,每個(gè)線程創(chuàng)建一個(gè)simpleDateFormat 對(duì)象------占用內(nèi)存大,性能低;
線程池,容量為16,設(shè)置simpleDateFormat為每個(gè)線程的ThreadLocal變量,一共就16個(gè)simpleDateFormat對(duì)象。

8.2、ThreadLocal是用來(lái)解決共享資源的多線程訪問(wèn)問(wèn)題嗎?

不是。ThreadLocal存儲(chǔ)的對(duì)象是線程獨(dú)享的,不是共享資源。
注意:當(dāng)放入ThreadLocal中的對(duì)象是static修飾的,那么ThreadLocal并不能解決線程安全問(wèn)題。

ThreadLocal 是通過(guò)讓每個(gè)線程獨(dú)享自己的副本,避免了資源的競(jìng)爭(zhēng)。
synchronized 主要用于臨界資源的分配,在同一時(shí)刻限制最多只有一個(gè)線程能訪問(wèn)該資源。

8.3、多個(gè)ThreadLocal在Thread的threadlocals里是如何存儲(chǔ)的?

Thread>ThreadLocalMap>ThreadLocal

[圖片上傳失敗...(image-fbb76b-1689324444603)]

一個(gè) Thread 里面只有一個(gè)ThreadLocalMap ,而在一個(gè) ThreadLocalMap 里面卻可以有很多的 ThreadLocal,每一個(gè) ThreadLocal 都對(duì)應(yīng)一個(gè) value。

如果在一個(gè)想在Thread中保存多個(gè)ThreadLocal對(duì)象,那么直接new多個(gè)ThreadLocal存儲(chǔ)對(duì)象即可。

ThreadLocal threadLocal1 = new ThreadLocal();        
threadLocal1.set("string1");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("string2");

8.4、內(nèi)存泄露

8.4.1、key的內(nèi)存泄露:

ThreadLocalMap里的Entry中的key是弱引用。

如果在Entry中強(qiáng)引用了ThreadLocal,盡管執(zhí)行 ThreadLocal instance = null,
根據(jù)可達(dá)性分析算法,該ThreadLocal對(duì)象仍然是可達(dá)的,就不會(huì)被回收。

8.4.2、value的內(nèi)存泄露:

正常情況下,當(dāng)線程終止,key 所對(duì)應(yīng)的 value 是可以被正常垃圾回收的,因?yàn)闆](méi)有任何強(qiáng)引用存在了。但是有時(shí)線程的生命周期是很長(zhǎng)的,如果線程遲遲不會(huì)終止,那么可能 ThreadLocal 以及它所對(duì)應(yīng)的 value 早就不再有用了。在這種情況下,我們應(yīng)該保證它們都能夠被正常的回收。

8.4.3、避免內(nèi)存泄露:

調(diào)用 ThreadLocal 的 remove 方法。調(diào)用這個(gè)方法就可以刪除對(duì)應(yīng)的 value 對(duì)象,可以避免內(nèi)存泄漏。

以上內(nèi)容為個(gè)人學(xué)習(xí)理解,如有問(wèn)題,歡迎在評(píng)論區(qū)指出。
部分內(nèi)容截取自網(wǎng)絡(luò),如有侵權(quán),聯(lián)系作者刪除。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,011評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,263評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,543評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,323評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,874評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,095評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,605評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,551評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,720評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,961評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,358評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,612評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,330評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,690評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容