java后端知識點梳理——多線程與高並發


進程與線程

進程是一個“執行中的程序”,是系統進行資源分配和調度的一個獨立單位

線程是進程的一個實體,一個進程中一般擁有多個線程。

線程和進程的區別

  • 進程是操作系統分配資源的最小單元,線程是操作系統調度的最小單元。
  • 進程有獨立的地址空間,相互不影響,線程只是進程的不同執行路徑
  • 線程沒有獨立的地址空間,多進程的程序比多線程的程序健壯
  • 進程的切換比線程的切換開銷大,所以線程上下文的切換比進程上下文切換要快很多。

為什么線程上下文切換比進程上下文切換快?

  • 進程切換時,涉及到當前進程的CPU環境的保存和新被調度運行進程的CPU環境的設置
  • 線程切換時,僅需要保存和設置少量的寄存器內容,不涉及存儲管理方面的操作

線程沒有獨立的地址空間,哪有獨屬於自己的資源嗎?

有,通過ThreadLocal可以存儲線程的特有對象,也就是屬於當前線程的資源。

進程的通信方式

通常有管道(包括無名管道和命名管道)、消息隊列、信號量、共享存儲、Socket、Streams等

進程之間常見的通信方式一般有:

  • 通過使用套接字Socket來實現不同機器間的進程通信
  • 通過映射一段可以被多個進程訪問的共享內存來進行通信
  • 通過寫進程和讀進程利用管道進行通信

線程的通信方式

  • volatile
  • 使用Object類的wait()notify() 方法
  • CountDownLatch方式實現
  • join + synchronize/Reentrantlock

volatile

volatile有兩大特性,一是可見性,二是有序性,禁止指令重排序,其中可見性就是可以讓線程之間進行通信。

volatile語義保證線程可見性有兩個原則保證

  • 所有volatile修飾的變量一旦被某個線程更改,必須立即刷新到主內存
  • 所有volatile修飾的變量在使用之前必須重新讀取主內存的值

volatile保證可見性原理圖

工作內存2能夠感知到工作內存1更新a值是靠的總線,工作內存1在將值刷新的主內存時必須經過總線,總線就能告知其他線程有值被改變,那么其他線程就會主動讀取主內存的值來更新。

public class VolatileDemo {

    private static volatile boolean flag = true;

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (flag){
                        System.out.println("trun on");
                        flag = false;
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (!flag){
                        System.out.println("trun off");
                        flag = true;
                    }
                }
            }
        }).start();
    }
}

如上代碼所示,輸出結果會在trun ontrun off之間切換,這是因為經過了總線,線程之間相互通信,知道flag值的改變。如果去掉了volatile,在線程切換一定次數后就會發現感知不到flag值的變化了。

wait和notify方法

wait和notify方法來實現等待/通知機制.在一個線程內調用該線程鎖對象的wait方法,線程將進入等待隊列進行等待直到被通知或者被喚醒,完成線程之間的通信交互。

CountDownLatch

寫兩個線程,線程1添加10個元素到容器中, 線程2實現監控元素的個數,當個數到5個時,線程2給出提示並結束

public class WithoutVolatile1 {
    private volatile List list=Collections.synchronizedList (new LinkedList <> ());
    public  void add(Object i){
        list.add (i);
    }
    public  int size(){
        return list.size ();
    }

    public static void main(String[] args) {
        WithoutVolatile1 w=new WithoutVolatile1();
        //為什么要創建兩個門閂呢?
        //假如你用一個CountDownLatch 的話會導致線程二在執行的同時出現了CPU的上下文切換,導致數據的不一致,可以多試幾次就會發現問題
        CountDownLatch countDownLatch = new CountDownLatch (1);
        CountDownLatch latch = new CountDownLatch (1);
        Thread thread = new Thread (() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    if (w.size ( ) == 5) {
                        countDownLatch.countDown ( );
                        latch.await ();
                    }
                    w.add (i);
                    System.out.println("添加 " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace ( );
            }
        });
        Thread thread1 = new Thread (() -> {
            try {
                if(w.size ()!=5){
                    countDownLatch.await ( );
                }
                System.out.println ("監聽到了,結束。。。。");
                latch.countDown ();
            } catch (InterruptedException e) {
                e.printStackTrace ( );
            }
        });
        thread.start ();
        thread1.start ();

    }
}

線程的狀態有哪些?

線程的狀態包括 新建狀態,運行狀態,阻塞等待狀態和消亡狀態。其中阻塞等待狀態又分為BLOCKED, WAITING和TIMED_WAITING狀態。

new(新建)->ready(可運行)->running(運行中)->blocked(阻塞狀態)->waiting(等待狀態)->time_waiting(超時等待)->terminated(終止狀態)

  • 線程創建后處於new狀態,調用start()方法后開始運行,處於ready狀態,處於ready狀態的線程獲得了cpu時間片timeslice后就處於running狀態
  • 線程執行wait()方法之后進入waiting狀態,進入等待狀態后需要依靠其他線程的通知才能返回到運行狀態
  • time_waiting狀態相當於在等待基礎上增加了超時限制,當超時時間達到后線程返回到runnning狀態
  • 線程調用同步方法時,如果沒有獲取鎖,就進入blocked狀態
  • 線程執行了run()方法之后進入terminated狀態

多線程編程中常用的函數比較

sleep 和 wait 的區別:

  • sleep方法:是Thread類的靜態方法,當前線程將睡眠n毫秒,線程進入阻塞狀態。當睡眠時間到了,會解除阻塞,進入可運行狀態,等待CPU的到來。睡眠不釋放鎖(如果有的話)。
  • wait方法:是Object的方法,必須與synchronized關鍵字一起使用,線程進入阻塞狀態,當notify或者notifyall被調用后,會解除阻塞。但是,只有重新占用互斥鎖之后才會進入可運行狀態。睡眠時,會釋放互斥鎖。

join 方法:當前線程調用,則其它線程全部停止,等待當前線程執行完畢,接着執行。

yield 方法:該方法使得線程放棄當前分得的 CPU 時間。但是不使線程阻塞,即線程仍處於可執行狀態,隨時可能再次分得 CPU 時間。

死鎖

死鎖:多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

造成死鎖的原因可以概括成三句話:

  • 當前線程擁有其他線程需要的資源
  • 當前線程等待其他線程已擁有的資源
  • 都不放棄自己擁有的資源

死鎖的產生必須滿足如下四個必要條件:

  • 資源互斥:一個資源每次只能被一個線程使用
  • 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放
  • 不剝奪條件:線程已經獲得的資源,在未使用完之前,不能強行剝奪
  • 循環等待條件:若干線程之間形成一種頭尾相接的循環等待資源關系

避免死鎖的方法

  • 固定加鎖的順序(針對鎖順序死鎖)
  • 開放調用(針對對象之間協作造成的死鎖)
  • 使用定時鎖-->tryLock()
    • 如果等待獲取鎖時間超時,則拋出異常而不是一直等待!

固定加鎖的順序

先看一個例子

// 轉賬
public static void transferMoney(Account fromAccount,
                                 Account toAccount,
                                 DollarAmount amount)
        throws InsufficientFundsException {

    // 鎖定匯賬賬戶
    synchronized (fromAccount) {
        // 鎖定來賬賬戶
        synchronized (toAccount) {
            // 判余額是否大於0
            if (fromAccount.getBalance().compareTo(amount) < 0) {
                throw new InsufficientFundsException();
            } else {
                // 匯賬賬戶減錢
                fromAccount.debit(amount);
                // 來賬賬戶增錢
                toAccount.credit(amount);
            }
        }
    }
}

有可能會發生死鎖:

  • 如果兩個線程同時調用transferMoney()
  • 線程A從X賬戶向Y賬戶轉賬
  • 線程B從賬戶Y向賬戶X轉賬
  • 那么就會發生死鎖。

A:transferMoney(myAccount,yourAccount,10);

B:transferMoney(yourAccount,myAccount,20);

問題: 加鎖順序不一致

解決方法::如果所有線程以固定的順序來獲得鎖,那么程序中就不會出現鎖順序死鎖問題!

public class InduceLockOrder {

    // 額外的鎖、避免兩個對象hash值相等的情況(即使很少)
    private static final Object tieLock = new Object();

    public void transferMoney(final Account fromAcct,
                              final Account toAcct,
                              final DollarAmount amount)
            throws InsufficientFundsException {
        class Helper {
            public void transfer() throws InsufficientFundsException {
                if (fromAcct.getBalance().compareTo(amount) < 0)
                    throw new InsufficientFundsException();
                else {
                    fromAcct.debit(amount);
                    toAcct.credit(amount);
                }
            }
        }
        // 得到鎖的hash值
        int fromHash = System.identityHashCode(fromAcct);
        int toHash = System.identityHashCode(toAcct);

        // 根據hash值來上鎖
        if (fromHash < toHash) {
            synchronized (fromAcct) {
                synchronized (toAcct) {
                    new Helper().transfer();
                }
            }

        } else if (fromHash > toHash) {// 根據hash值來上鎖
            synchronized (toAcct) {
                synchronized (fromAcct) {
                    new Helper().transfer();
                }
            }
        } else {// 額外的鎖、避免兩個對象hash值相等的情況(即使很少)
            synchronized (tieLock) {
                synchronized (fromAcct) {
                    synchronized (toAcct) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }
}

得到對應的hash值來固定加鎖的順序,這樣我們就不會發生死鎖的問題了!

補充知識點:

鎖排序法:(必須回答出來的點)
指定獲取鎖的順序,比如某個線程只有獲得A鎖和B鎖,才能對某資源進行操作,在多線程條件下,如何避免死鎖?

通過指定鎖的獲取順序,比如規定,只有獲得A鎖的線程才有資格獲取B鎖,按順序獲取鎖就可以避免死鎖。這通常被認為是解決死鎖很好的一種方法。

開放調用

在協作對象之間發生死鎖的例子中,主要是因為在調用某個方法時就需要持有鎖,並且在方法內部也調用了其他帶鎖的方法!

如果在調用某個方法時不需要持有鎖,那么這種調用被稱為開放調用!

注意:同步代碼塊最好僅被用於保護那些涉及共享狀態的操作!

class CooperatingNoDeadlock {
    @ThreadSafe
    class Taxi {
        @GuardedBy("this") private Point location, destination;
        private final Dispatcher dispatcher;

        public Taxi(Dispatcher dispatcher) {
            this.dispatcher = dispatcher;
        }

        public synchronized Point getLocation() {
            return location;
        }

        public synchronized void setLocation(Point location) {
            boolean reachedDestination;

            // 加Taxi內置鎖
            synchronized (this) {
                this.location = location;
                reachedDestination = location.equals(destination);
            }
            // 執行同步代碼塊后完畢,釋放鎖



            if (reachedDestination)
                // 加Dispatcher內置鎖
                dispatcher.notifyAvailable(this);
        }

        public synchronized Point getDestination() {
            return destination;
        }

        public synchronized void setDestination(Point destination) {
            this.destination = destination;
        }
    }

    @ThreadSafe
    class Dispatcher {
        @GuardedBy("this") private final Set<Taxi> taxis;
        @GuardedBy("this") private final Set<Taxi> availableTaxis;

        public Dispatcher() {
            taxis = new HashSet<Taxi>();
            availableTaxis = new HashSet<Taxi>();
        }

        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }

        public Image getImage() {
            Set<Taxi> copy;

            // Dispatcher內置鎖
            synchronized (this) {
                copy = new HashSet<Taxi>(taxis);
            }
            // 執行同步代碼塊后完畢,釋放鎖

            Image image = new Image();
            for (Taxi t : copy)
                // 加Taix內置鎖
                image.drawMarker(t.getLocation());
            return image;
        }
    }

    class Image {
        public void drawMarker(Point p) {
        }
    }

}

使用定時鎖

使用顯式Lock鎖,在獲取鎖時使用tryLock()方法。當等待超過時限的時候,tryLock()不會一直等待,而是返回錯誤信息。

使用tryLock()能夠有效避免死鎖問題。

死鎖檢測

JDK提供了兩種方式來給我們檢測:

  • JconsoleJDK自帶的圖形化界面工具,使用JDK給我們的的工具JConsole
  • Jstack是JDK自帶的命令行工具,主要用於線程Dump分析。

原子性,可見性與有序性

背景預覽

  • 原子性:要么執行,要么不執行,主要使用互斥鎖Synchronize或者lock來保證操作的原子性;
  • 可見性:一個線程對共享變量的修改,另—個線程能夠立刻看到。(具體的說:在變量修改后將新值同步回主內存,主要有兩種實現方式,一是volatile,被volatile修飾的變量發生修改后會立即刷新到主內存;二是使用Synchronize或者lock,當一個變量unlock之前會將變量的修改刷新到主內存中);
  • 有序性:程序執行的順序按照代碼的先后順序執行。(具體的說:在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序不會影響單線程的執行結果,卻會影響多線程並發執行的正確性。主要有兩種方式確保有序性:volatile 和 Synchronize 關鍵字,volatile是通過添加內存屏障的方式來禁止指令重排序,也就是重排序是不能把后面的指令放到內存屏障之前執行;Synchronize是保證同一時刻有且只有一個線程執行同步代碼,類似於串聯順序執行代碼)。

原子性:

定義:對於涉及到共享變量訪問的操作,若該操作從執行線程以外的任意線程來看是不可分割的,那么該操作就是原子操作,該操作具有原子性。即,其它線程不會“看到”該操作執行了部分的中間結果。

舉例:銀行轉賬流程中,A賬戶減少了100元,那么B賬戶就會多100元,這兩個動作是一個原子操作。我們不會看到A減少了100元,但是B余額保持不變的中間結果。

原子性的實現方式:

  • 利用鎖的排他性,保證同一時刻只有一個線程在操作一個共享變量
  • 利用CAS(Compare And Swap)保證
  • Java語言規范中,保證了除long和double型以外的任何變量的寫操作都是原子操作
  • Java語言規范中又規定,volatile關鍵字修飾的變量可以保證其寫操作的原子性

關於原子性,你應該注意的地方:

  • 原子性針對的是多個線程的共享變量,所以對於局部變量來說不存在共享問題,也就無所謂是否是原子操作
  • 單線程環境下討論是否是原子操作沒有意義
  • volatile關鍵字僅僅能保證變量寫操作的原子性,不保證復合操作,比如說讀寫操作的原子性

可見性:

定義:可見性是指一個線程對於共享變量的更新,對於后續訪問該變量的線程是否可見的問題。

為了闡述可見性問題,我們先來簡單介紹處理器緩存的概念。

現代處理器處理速度遠大於主內存的處理速度,所以在主內存和處理器之間加入了寄存器,高速緩存,寫緩沖器以及無效化隊列等部件來加速內存的讀寫操作。也就是說,我們的處理器可以和這些部件進行讀寫操作的交互,這些部件可以稱為處理器緩存。

處理器對內存的讀寫操作,其實僅僅是與處理器緩存進行了交互。一個處理器的緩存上的內容無法被另外一個處理器讀取,所以另外一個處理器必須通過緩存一致性協議來讀取的其他處理器緩存中的數據,並且同步到自己的處理器緩存中,這樣保證了其余處理器對該變量的更新對於另外處理器是可見的。

在單處理器中,為什么也會出現可見性的問題呢?

單處理器中,由於是多線程並發編程,所以會存在線程的上下文切換,線程會將對變量的更新當作上下文存儲起來,導致其余線程無法看到該變量的更新。所以單處理器下的多線程並發編程也會出現可見性問題的。

可見性如何保證?

  • 當前處理器需要刷新處理器緩存,使得其余處理器對變量所做的更新可以同步到當前的處理器緩存中
  • 當前處理器對共享變量更新之后,需要沖刷處理器緩存,使得該更新可以被寫入處理器緩存中

有序性:

定義:有序性是指一個處理器上運行的線程所執行的內存訪問操作在另外一個處理器上運行的線程來看是否有序的問題。

重排序:

為了提高程序執行的性能,Java編譯器在其認為不影響程序正確性的前提下,可能會對源代碼順序進行一定的調整,導致程序運行順序與源代碼順序不一致。

重排序是對內存讀寫操作的一種優化,在單線程環境下不會導致程序的正確性問題,但是多線程環境下可能會影響程序的正確性。

重排序舉例:Instance instance = new Instance()都發生了啥?

具體步驟如下所示三步:

  • 在堆內存上分配對象的內存空間
  • 在堆內存上初始化對象
  • 設置instance指向剛分配的內存地址

第二步和第三步可能會發生重排序,導致引用型變量指向了一個不為null但是也不完整的對象。(在多線程下的單例模式中,我們必須通過volatile來禁止指令重排序)

解析:

  • 原子性是一組操作要么完全發生,要么沒有發生,其余線程不會看到中間過程的存在。注意,原子操作+原子操作不一定還是原子操作。
  • 可見性是指一個線程對共享變量的更新對於另外一個線程是否可見的問題。
  • 有序性是指一個線程對共享變量的更新在其余線程看起來是按照什么順序執行的問題。
  • 可以這么認為,原子性 + 可見性 -> 有序性

synchronized關鍵字

synchronized是Java中的一個關鍵字,是一個內部鎖。它可以使用在方法上和方法塊上,表示同步方法和同步代碼塊。在多線程環境下,同步方法或者同步代碼塊在同一時刻只允許有一個線程在執行,其余線程都在等待獲取鎖,也就是實現了整體並發中的局部串行。

內部鎖底層實現:

  • 進入時,執行monitorenter,將計數器+1,釋放鎖monitorexit時,計數器-1
  • 當一個線程判斷到計數器為0時,則當前鎖空閑,可以占用;反之,當前線程進入等待狀態

synchronized內部鎖對原子性的保證:

在第一個線程獲取到鎖之后,在他執行完之前不允許其他的線程獲取鎖並操作共享數據,從而保證了程序的原子性。synchronized保證原子性的原理,synchronized保證只有一個線程拿到鎖,能夠進入同步代碼塊

synchronized內部鎖對可見性的保證:

synchronized內部鎖通過寫線程沖刷處理器緩存和讀線程刷新處理器緩存保證可見性。

  • 獲得鎖之后,需要刷新處理器緩存,使得前面寫線程所做的更新可以同步到本線程。
  • 釋放鎖需要沖刷處理器緩存,使得當前線程對共享數據的改變可以被推送到下一個線程處理器的高速緩沖中。

synchronized內部鎖對有序性的保證:

由於原子性和可見性的保證,使得寫線程在臨界區中所執行的一系列操作在讀線程所執行的臨界區看起來像是完全按照源代碼順序執行的,即保證了有序性。


注:內部鎖可以使用在方法上和代碼塊上,被內部鎖修飾的區域又叫做臨界區


公平鎖和非公平鎖

  • 公平調度方式:

按照申請的先后順序授予資源的獨占權。

  • 非公平調度方式:

在該策略中,資源的持有線程釋放該資源的時候,等待隊列中一個線程會被喚醒,而該線程從被喚醒到其繼續執行可能需要一段時間。在該段時間內,新來的線程(活躍線程)可以先被授予該資源的獨占權。

如果新來的線程占用該資源的時間不長,那么它完全有可能在被喚醒的線程繼續執行前釋放相應的資源,從而不影響該被喚醒的線程申請資源。

優缺點分析:

非公平調度策略:

  • 優點:吞吐率較高,單位時間內可以為更多的申請者調配資源
  • 缺點:資源申請者申請資源所需的時間偏差可能較大,並可能出現線程飢餓的現象

公平調度策略:

  • 優點:線程申請資源所需的時間偏差較小;不會出現線程飢餓的現象;適合在資源的持有線程占用資源的時間相對長或者資源的平均申請時間間隔相對長的情況下,或者對資源申請所需的時間偏差有所要求的情況下使用;
  • 缺點:吞吐率較小

JVM對synchronized內部鎖的調度:

JVM對內部鎖的調度是一種非公平的調度方式,JVM會給每個內部鎖分配一個入口集(Entry Set),用於記錄等待獲得相應內部鎖的線程。當鎖被持有的線程釋放的時候,該鎖的入口集中的任意一個線程將會被喚醒,從而得到再次申請鎖的機會。被喚醒的線程等待占用處理器運行時可能還有其他新的活躍線程與該線程搶占這個被釋放的鎖.

volatile關鍵字

volatile關鍵字是一個輕量級的鎖,可以保證可見性和有序性,但是不保證原子性。

  • volatile 可以保證主內存和工作內存直接產生交互,進行讀寫操作,保證可見性
  • volatile 僅能保證變量寫操作的原子性,不能保證讀寫操作的原子性。
  • volatile可以禁止指令重排序(通過插入內存屏障),典型案例是在單例模式中使用。

volatile變量的開銷:

volatile不會導致線程上下文切換,但是其讀取變量的成本較高,因為其每次都需要從高速緩存或者主內存中讀取,無法直接從寄存器中讀取變量。

volatile在什么情況下可以替代鎖?

volatile是一個輕量級的鎖,適合多個線程共享一個狀態變量,鎖適合多個線程共享一組狀態變量。可以將多個線程共享的一組狀態變量合並成一個對象,用一個volatile變量來引用該對象,從而替代鎖。

ReentrantLock和synchronized的區別

  • ReentrantLock是顯示鎖,其提供了一些內部鎖不具備的特性,但並不是內部鎖的替代品。顯式鎖支持公平和非公平的調度方式,默認采用非公平調度。
  • synchronized 內部鎖簡單,但是不靈活。顯示鎖支持在一個方法內申請鎖,並且在另一個方法里釋放鎖。顯示鎖定義了一個tryLock()方法,嘗試去獲取鎖,成功返回true,失敗並不會導致其執行的線程被暫停而是直接返回false,即可以避免死鎖。

線程池

線程池就是創建若干個可執行的線程放入一個池(容器)中,有任務需要處理時,會提交到線程池中的任務隊列,處理完之后線程並不會被銷毀,而是仍然在線程池中等待下一個任務。

ThreadPoolExecutor有七個參數,分別是

  • corePoolSize:線程池中的常駐核心線程數
  • maximumPoolSize:線程池中能夠容納同時執行的最大線程數,此值必須大於等於1
  • keepAliveTimes:多余的空閑線程存活時間。當線程池的數量超過corePoolSize時,當時間達到keepAliveTime時,多余的線程會被銷毀直到剩下corePoolSize個線程為止
  • unit:keepAliveTime的單位
  • workQueue:任務隊列,被提交但為執行的任務
  • threadFactory:表示生成線程池中工作線程的線程工廠,用於創建線程,一般默認即可
  • handler:拒絕策略,表示當隊列滿了,並且工作線程大於等於線程池的最大線程數時,如何拒絕請求執行的runnable的策略

四個拒絕策略

  • AbortPolicy(默認):直接拋出RejectExecutionException阻止運行
  • CallerRunsPolicy:不會拋出異常,也不會拋棄任務,而是將任務回退給調用者
  • discardOldestPolicy:拋棄隊列中等待最久的任務,然后把當前任務加入隊列中嘗試再次提交當前任務
  • discardPolicy:默默丟棄掉無法處理的任務,不會拋出異常也不做任何處理

關於最大線程數maximumPoolSize的設置

maximumPoolSize一般設置為本機線程數,查看本機線程數的方法為Runtime.getRuntime().availableProcessors

常見的線程池類型:

newCachedThreadPool( )

  • 核心線程池大小為0,最大線程池大小不受限,來一個創建一個線程
  • 適合用來執行大量耗時較短且提交頻率較高的任務

newFixedThreadPool( )

  • 固定大小的線程池
  • 當線程池大小達到核心線程池大小,就不會增加也不會減小工作者線程的固定大小的線程池

newSingleThreadExecutor( )

  • 便於實現單(多)生產者-消費者模式

從排隊策略看線程池工作原理

排隊策略

當我們向線程池提交任務的時候,需要遵循一定的排隊策略,具體策略如下:

  • 如果運行的線程少於corePoolSize,則Executor始終首選添加新的線程,而不進行排隊
  • 如果運行的線程等於或者多於corePoolSize,則Executor始終首選將請求加入隊列,而不是添加新線程
  • 如果無法將請求加入隊列,即隊列已經滿了,則創建新的線程,除非創建此線程超出maxinumPoolSize,在這種情況下,任務默認將被拒絕。

線程池工作原理

從阻塞隊列開線程池已滿會發生什么

常見的阻塞隊列

ArrayBlockingQueue:

  • 內部使用一個數組作為其存儲空間,數組的存儲空間是預先分配的
  • 優點是 put 和 take操作不會增加GC的負擔(因為空間是預先分配的)
  • 缺點是 put 和 take操作使用同一個鎖,可能導致鎖爭用,導致較多的上下文切換。
  • ArrayBlockingQueue適合在生產者線程和消費者線程之間的並發程序較低的情況下使用。

LinkedBlockingQueue:

  • 是一個無界隊列(其實隊列長度是Integer.MAX_VALUE)
  • 內部存儲空間是一個鏈表,並且鏈表節點所需的存儲空間是動態分配的
  • 優點是 put 和 take 操作使用兩個顯式鎖(putLock和takeLock)
  • 缺點是增加了GC的負擔,因為空間是動態分配的。
  • LinkedBlockingQueue適合在生產者線程和消費者線程之間的並發程序較高的情況下使用。

SynchronousQueue:

  • SynchronousQueue可以被看做一種特殊的有界隊列。生產者線程生產一個產品之后,會等待消費者線程來取走這個產品,才會接着生產下一個產品,適合在生產者線程和消費者線程之間的處理能力相差不大的情況下使用。

前邊介紹newCachedThreadPool時候說,這個線程池來一個線程就創建一個,這是因為其內部隊列使用了SynchronousQueue,所以不存在排隊。

注意點

  • 使用JDK提供的快捷方式創建線程池,比如說newCachedThreadPool會出現一些內存溢出的問題,因為隊列可以被塞入很多任務。所以,大多數情況下,我們都應該自定義線程池。
  • 線程池提供了一些監控API,可以很方便的監控當前以及塞進隊列的任務數以及當前線程池已經完成的任務數等。

CountDownLatch和CyclicBarrier

兩個關鍵字經常放在一起比較和考察

CountDownLatch是一個倒計時協調器,它可以實現一個或者多個線程等待其余線程完成一組特定的操作之后,繼續運行。

CountDownLatch的內部實現如下:

  • CountDownLatch內部維護一個計數器,CountDownLatch.countDown()每被執行一次都會使計數器值減少1。
  • 當計數器不為0時,CountDownLatch.await()方法的調用將會導致執行線程被暫停,這些線程就叫做該CountDownLatch上的等待線程。
  • CountDownLatch.countDown()相當於一個通知方法,當計數器值達到0時,喚醒所有等待線程。當然對應還有指定等待時間長度的CountDownLatch.await( long , TimeUnit)方法。

CyclicBarrier是一個柵欄,可以實現多個線程相互等待執行到指定的地點,這時候這些線程會再接着執行,在實際工作中可以用來模擬高並發請求測試。

可以認為是這樣的,當我們爬山的時候,到了一個平坦處,前面隊伍會稍作休息,等待后邊隊伍跟上來,當最后一個爬山伙伴也達到該休息地點時,所有人同時開始從該地點出發,繼續爬山。

CyclicBarrier的內部實現如下:

  • 使用CyclicBarrier實現等待的線程被稱為參與方(Party),參與方只需要執行CyclicBarrier.await()就可以實現等待,該柵欄維護了一個顯示鎖,可以識別出最后一個參與方,當最后一個參與方調用await()方法時,前面等待的參與方都會被喚醒,並且該最后一個參與方也不會被暫停。
  • CyclicBarrier內部維護了一個計數器變量count = 參與方的個數,調用await方法可以使得count -1。當判斷到是最后一個參與方時,調用singalAll喚醒所有線程。

ThreadLocal

使用ThreadLocal維護變量時,其為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立的改變自己的副本,而不會影響其他線程對應的副本。

ThreadLocal內部實現機制:

  • 每個線程內部都會維護一個類似HashMap的對象,稱為ThreadLocalMap,里邊會包含若干了Entry(K-V鍵值對),相應的線程被稱為這些Entry的屬主線程
  • Entry的Key是一個ThreadLocal實例,Value是一個線程特有對象。Entry的作用是為其屬主線程建立起一個ThreadLocal實例與一個線程特有對象之間的對應關系
  • Entry對Key的引用是弱引用;Entry對Value的引用是強引用。

Atmoic

引入

經典問題:i++是線程安全的嗎?

i++操作並不是線程安全的,它是一個復合操作,包含三個步驟:

拷貝i的值到臨時變量
臨時變量++操作
拷貝回原始變量i

這是一個復合操作,不能保證原子性,所以這不是線程安全的操作。

那么如何實現原子自增等操作呢?

這里就用到了JDK在java.util.concurrent.atomic包下的AtomicInteger等原子類了。AtomicInteger類提供了getAndIncrement和incrementAndGet等原子性的自增自減等操作。Atomic等原子類內部使用了CAS來保證原子性。

后續更新

  • 什么是happened-before原則?
  • JVM虛擬機對內部鎖有哪些優化?
  • 如何進行無鎖化編程?
  • CAS以及如何解決ABA問題?
  • AQS(AbstractQueuedSynchronizer)的原理與實現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM