Java基礎知識複習(三)

5 Java併發

synchronized

synchronized是jdk提供的jvm層面的同步機制。他解決的是多線程之間訪問共享資源的同步問題,它保證再它修飾的方法或代碼塊同一時間只有一個線程執行。

在早期的Java八本中,synchronized屬於重量級鎖,效率低下,因爲監視器鎖(monitor)是依賴於底層的操作系統的 Mutex Lock 來實現的,Java的線程是映射到操作系統的原生線程之上的。如果需要掛起或者喚醒一個線程,都需要操作系統幫忙完成,掛起或喚醒一個線程都需要從用戶態轉換爲內核態,這個狀態之間的轉換需要相對長的時間,時間成本相對較高。

在Java6之後,Java官方對從JVM層面對synchronized做了較大優化,所以現在synchronized鎖效率也挺不錯了。

如何使用 synchronized 關鍵字

三種方式:

  • 修飾實例方法:作用與當前對象實例的鎖,進入同步代碼前要獲得當前對象實例的鎖。
  • 修飾靜態方法:修飾靜態方法其實就是給當前類加鎖,因爲靜態方法是屬於類的。所以如果一個線程 A 調用一個實例對象的非靜態 synchronized 方法,而線程 B 需要調用這個實例對象所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖
  • 修飾代碼塊:指定枷鎖對象,對給定對象枷鎖,進入同步代碼塊前要獲得給定對象的鎖。

synchronized關鍵字底層原理

synchronized代碼塊底層原理

先編寫一份測試代碼:

public class Test {
    public static void main(String[] args) {
        synchronized (Test.class){
            System.out.printf("current thread name: %s\n",Thread.currentThread().getName());
        }
    }
}

然後使用命令對Test.class進行反編譯:輸入javap -c -v -s -l Test.class

在這裏插入圖片描述

可以看到,在進入synchronized同步代碼塊時,底層字節碼編譯出來的指令爲monitorenter,而退出synchronized同步代碼塊時,編譯的指令爲 monitorexit。當執行monitorenter指令時,當前線程將視圖獲取objectref(即對象鎖) 所對應的 monitor 的持有權,在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的 ,它是一個c++對象:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;        
    _waiters      = 0,
    _recursions   = 0;        // 鎖的重入次數
    _object       = NULL;     // synchronized的鎖對象
    _owner        = NULL;     // 擁有該monitor的線程
    _WaitSet      = NULL;	  // 處於wait狀態的線程會被加入到此set集合中
    _WaitSetLock  = 0 ;	     
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;	  // 多線程競爭鎖時的單向隊列
    FreeNext      = NULL ;    
    _EntryList    = NULL ;    // 競爭失敗後,陷入阻塞的線程會被加入到此隊列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

如果對象鎖monitor的_recursions計數器爲0,那麼線程就可以成功的獲取到這個鎖,此時 _recursions計數器置爲1,如果當前線程已經用了monitor的持有權的,那它也可以重入這個monitor,此時 _recursions計數器也會+1,倘若其他線程已經擁有monitor的持有權,那麼當前線程就會進入阻塞狀態,直到當前持有monitor持有權的線程執行完畢,即執行完monitorexit指令,此時持有monitor的線程就會釋放monitor,並將monitor的計數器歸0,此時其他的線程又可以爭搶monitor的持有權了。值得注意的是,編譯器將會確保無論方法以哪種形式結束(正常退出或者異常退出),方法中執行過的monitorenter指令都有一條對應的monitorexit指令可以正確配對執行。編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。

synchronized方法底層原理

在這裏插入圖片描述

synchronized修飾的方法並沒有 monitorenter 指令和 monitorexit 指令修飾,它的同步形式是隱式的,取而代之的是一個ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 通過該ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。

Synchroized和ReentrantLock的區別

  • Synchroized是基於JVM層面的同步機制,而ReentrantLock是基於Java API層面提供的同步機制。
  • Synchroized和Reentrantlock都屬於可重入鎖。
  • ReetrantLock提供了比Synchronized更高級的功能:
    • 公平鎖
    • 更方便的線程間的通信(Condition)
    • 等待可中斷(在線程等待獲取鎖的時候可以被中斷)

樂觀鎖

樂觀鎖對共享的數據很樂觀,認爲不會發生線程安全的問題,從而不給數據加鎖。樂觀鎖適用於讀多寫少的環境。常見的例子是mysql的更新使用version控制。

悲觀鎖

悲觀鎖對共享的數據很悲觀,認爲無論什麼時候都有可能發生線程安全的問題,所以在每次讀寫數據的時候都會加鎖。

Synchronized屬於悲觀鎖。

獨佔鎖

鎖一次只能被一個線程佔用使用。

Synchronized和ReetrantLock都是獨佔鎖。

共享鎖

鎖可以被多個線程持有。

對於ReentrantReadWriteLock而言,它的讀鎖是共享鎖,寫鎖是獨佔鎖。

公平鎖

公平鎖指根據線程在隊列中的優先級獲取鎖,比如線程優先加入阻塞隊列,那麼線程就優先獲取鎖。

非公平鎖

非公平鎖指在獲取鎖的時候,每個線程都會去爭搶,並且都有機會獲取到鎖,無關線程的優先級。

可重入鎖(遞歸鎖)

一個線程獲取到鎖後,如果繼續遇到被相同鎖修飾的資源,那麼就可以繼續獲取該鎖。

Synchronized和Reentrantlock都是可重入鎖。

偏向鎖

在線程獲取偏向鎖的時候,jvm會判斷對象MarkWord裏偏向線程的ID是否爲當前線程ID。

如果是,說明當前鎖對象處於偏向狀態。

如果不是,則JVM嘗試CAS把對象的MarkWord的偏向線程ID設置爲當前線程ID,

如果設置成功,那麼對象偏向當前線程,並將當前對象的鎖標誌位改爲01。

如果設置失敗,則說明多線程競爭,將撤銷偏向鎖,升級爲輕量級鎖。

偏向鎖使用與單線程無所競爭環境(單線程環境)

所以當只有一個線程的時候,默認是偏向鎖。

輕量級鎖

在線程獲取對象鎖時,JVM首先會判斷對象是否爲無鎖狀態(無鎖狀態標誌位爲01)。

如果對象是無鎖狀態,那麼將在線程的棧幀中開闢一塊空間用於存儲對象的MarkWord,然後將對象的MarkWord複製到棧幀空間去,並使用CAS更新對象的MakrWord爲指向線程棧幀的指針。

如果更新成功,那麼當前線程獲取鎖成功,並修改對象的MarkWord標誌位爲00。

如果更新失敗,那麼JVM會判斷對象的MarkWord是否已經指向線程的棧幀。

如果已經指向,那麼線程直接執行同步代碼。否則,說明多個線程競爭,將inflate爲重量級鎖。

輕量級鎖使用於多線程無鎖競爭環境(多線程輪流執行,並不會發生衝突)

自旋鎖

在爭奪鎖過程中,線程不會停止獲取鎖,二十通過CAS不斷的判斷線程是否符合獲取鎖的條件。

自適應自旋鎖

自旋鎖意味着線程會不斷的消耗cpu資源,短時間還行,長時間意味着資源的浪費。所以自適應自旋鎖會有一個自旋的生命週期,過了這個生命週期,線程將不在自旋。

鎖消除

鎖消除屬於Java編譯器對程序的一種優化機制。鎖消除是指當JVM的JIT編譯器檢測出一些已經加鎖的代碼不可能出現共享的數據存在競爭的問題,會消除這樣的鎖。鎖消除的依據來源於逃逸分析算法。如果判斷到一段代碼,在堆上的數據不會逃逸出去被其他線程訪問到,那麼就把它們當作棧上的數據,爲線程私有的,所以無需加鎖。

鎖粗化

當虛擬機檢測一系列連續的操作都對同一個連續域連續加鎖,那麼它會把加鎖的範圍擴大至整個操作的序列外部,保證只加一次鎖

 public String t(){
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            stringBuffer.append(i); // stringBuffer對每一個appen方法都會加鎖,如果執行100次,就是100次加鎖,顯然不太可能
        }
        return stringBuffer.toString();
    }

經過鎖粗化的優化後,可能是這樣的:

public String t() {
    StringBuffer stringBuffer = new StringBuffer();
    synchronized (stringBuffer) { 
        for (int i = 0; i < 100; i++) {
            stringBuffer.append(i); // append方法不會再上鎖
        }
    }
    return stringBuffer.toString();
}

死鎖

死鎖是指多個線程在執行過程中,循環等待彼此佔用的資源而導致的無限期阻塞的情況。

產生死鎖的條件:

  • 互斥條件: 一個資源在一段時間內只能被一個進程所持有。
  • 不可搶佔條件:進程所持有的資源只能由進程自己主動釋放,其他資源的申請者不能向進程持有者搶奪資源
  • 佔有且申請條件:進程已經持有一個資源後,又申請其他資源,但是其他資源已經被其他線程所佔有。
  • 循環等待條件:進程1有進程2需要申請的資源,進程2有進程1需要申請的資源。那麼這兩個線程不停的等待彼此持有的資源,又不釋放已擁有的資源,陷入循環等待。

死鎖的例子:

public class DeadLock {

    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread t = new Thread(()->{
            synchronized (lock1){
                try {
                    System.out.println(Thread.currentThread().getName()+" get lock1 ing!");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+" after sleep 500ms!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" is waiting!");
                synchronized (lock2){
                    System.out.println(Thread.currentThread().getName()+" get lock2 ing!");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock2){
                try {
                    System.out.println(Thread.currentThread().getName()+" get lock2 ing!");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+" after sleep 500ms!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" is waiting!");
                synchronized (lock1){
                    System.out.println(Thread.currentThread().getName()+" get lock1 ing!");
                }
            }
        });
        t.start();
        t2.start();
    }
}

如何避免死鎖?

爲了避免死鎖,我們只需要破環產生死鎖條件的其中一個就行了,但是前兩個條件我們無法改變,就只能改變第三個或第四個條件:

  • 打破第三個條件:實現資源的有序分配。
  • 打破第四個體條件:設置等待超時時間。

volatile

Java中的內存模型

在jdk1.2之前,Java的內存模型實現總是從主內存讀取變量 ,是不需要特別的注意的。而在當前的Java內存模型下,線程可以把變量保存本地內存(線程私有)中,而不是從主內存中讀寫。也就是,當程序在運行過程中,會將運算需要的數據從主存複製一份到線程的本地內存當中,那麼線程進行計算時就可以直接從它的本地內存讀取數據和向其中寫入數據,當運算結束之後,再將本地內存中的數據刷新到主存當中。舉個簡單的例子,比如下面的這段代碼:

i = i + 1;

會先從主存當中讀取i的值,然後複製一份到本地內存當中,然後線程執行指令對i進行加1操作,然後將數據寫入本地內存,最後將本地內存中i最新的值刷新到主存當中。

這段代碼在單線程環境下是沒有問題的,但是在多線程中運行就有問題了。在多核CPU中,每條線程可能運行於不同的CPU中,因此每個線程都有自己的本地緩存。

比如現在我們有兩個線程,我們希望兩個線程執行完上面的語句,最終i的值爲2,但最終的結果卻不是如此。

最終的結果是1,一開始,兩個線程分別讀取i的值存入自己的本地緩存中,然後線程1進行+1操作,然後把i的最新值1寫入到內存中,但是此時線程2的本地緩存中i的值還是0,這樣再進行+1操作後,i的值還是1。這就是緩存一致性爲題。通常稱這種被多個線程訪問的變量爲共享變量。

如何解決緩存一致性問題

  • 加鎖,使用synchroized修飾方法或代碼塊,或者用ReentrantLock爲代碼塊加鎖。
  • 通過緩存一致性協議

併發編程中的三個概念

1.原子性

原子性:即一個操作,要麼執行完整,要麼就不執行,在執行的過程中不會被任何因素打斷。

2.可見性

可見性是指,在併發編程中,多個線程同時訪問同一個變量,其中某一個線程對這個變量進行修改,其他線程能立即看得到修改後的值。

3.有序性

有序性是指,程序執行循序按照代碼的先後循序執行。

舉個例子:

int a = 1;
int b = 3;
int c = a + b;

這裏的有三段代碼,正常按我們的理解來說,三段代碼的執行順序應該是1->2->3,但是在Java程序裏面,可能不會是這樣運行,也有可能是2->1->3

這是爲什麼呢,這裏發生的現象叫做指令重排

3.1 指令重排

處理器爲了提高程序運行效率,可能會對輸入的代碼進行優化,它不保證各個語句的執行順序是否和我們編寫的一樣,但是它會保證最終的結果是和正常執行的結果是一樣的。

既然上面說到編譯器爲了提高效率進行了指令重排,那麼它是怎麼保證最終結果和正常執行的結果是一致的呢?

int a = 1;
int b = 3;
int c = a + b;

假設我們先執行了3語句,但是3語句依賴着兩個值:a,b。那麼編譯器就會去尋找a,b的值,此時編譯器就會確保1、2語句在3語句之前執行,這樣就保證了最終結果和正常執行的結果是一致的。

volatile保證內存的可見性

上面我們說到,多個線程訪問同一個共享變量容易造成緩存一致性問題。那麼我們可以通過將變量聲明爲volatile,這就指示JVM,這個變量是不穩定的,每次使用它都必須從主內存中讀取。

說白了,volatile 關鍵字的主要作用就是保證變量的可見性然後還有一個作用是防止指令重排序。

volatile如何禁止指令重排序,能保證有序性嗎

volatile通過提供內存屏障來防止指令重排序。java內存模型回在每個volatile寫操作前後都會插入store指令,將工作內存中的變量同步會主內存。在每個volatile讀擦操作前後都會插入load指令,從主內存中讀取變量。

既然禁止了指令重排序,那麼在一定程度上就會保證有序性。

volatile保證原子性嗎

既然volatile能保證可見性,那麼volatile能保證對變量的操作是原子性嗎?

讓我們來看一個例子:

i++;

自增操作不是一個原子性操作,自增操作可以分解爲3個部分:

1、獲取到i的值

2、i的值+1

3、寫回內存

雖然這三個操作都是原子性操作,但是合起來它就不是一個原子性操作。volatile能保證可見性,是在對變量的操作完畢, 然後寫回主內存,其它內存才知道i的值被修改了,但是在上述的三個操作中,任意一個操作都可能有線程正在運行。

比如線程A正在執行2操作,但是現在線程B過來了,它讀取到的值還是i原來值,這時候線程A中i的值和線程B中i的值就不一致了,所以volatile不能保證原子性。要解決這個問題還是需要給操作加鎖。

ThreadLocal

ThreadLocal簡介

ThreadLocal爲每個線程都提供了一份相同的變量副本,每個線程都可以修改這個副本,但不用擔心於與其他線程發生數據衝突,實現了線程之間的數據隔離。

ThreadLocal示例

import java.text.SimpleDateFormat;
import java.util.Random;

/**
 * @Description : TODO
 * @Author : Weleness
 * @Date : 2020/06/16
 */
public class ThreadLocalExample implements Runnable{
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyyMMdd HHmm"));
    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(obj,""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name = "+Thread.currentThread().getName() + " default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        formatter.set(new SimpleDateFormat());
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }
}

output:

Thread Name = 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = y/M/d ah:mm
Thread Name = 1 default Formatter = yyyyMMdd HHmm
Thread Name = 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = y/M/d ah:mm
Thread Name= 2 formatter = y/M/d ah:mm
Thread Name = 3 default Formatter = yyyyMMdd HHmm
Thread Name = 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = y/M/d ah:mm
Thread Name= 4 formatter = y/M/d ah:mm
Thread Name = 5 default Formatter = yyyyMMdd HHmm
Thread Name = 6 default Formatter = yyyyMMdd HHmm
Thread Name = 7 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = y/M/d ah:mm
Thread Name= 6 formatter = y/M/d ah:mm
Thread Name = 8 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = y/M/d ah:mm
Thread Name= 7 formatter = y/M/d ah:mm
Thread Name = 9 default Formatter = yyyyMMdd HHmm
Thread Name= 9 formatter = y/M/d ah:mm

可以看到thread-0雖然已經改變了formatter的值,但是thread-1默認格式化程序與初始值相同,其他線程也一樣。

ThreadLocal原理

Thread類源碼入手

public
class Thread implements Runnable {
  /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

Thread類源碼有一個threadLoacls和一個inheritableThreadLocals變量,它們都是ThreadLocalMap的變量,ThreadLoaclMapThreadLoacl的一個靜態內部類,通過Entry類存儲值

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

可以把 ThreadLocalMap 理解爲ThreadLoacl類實現的定製化的HashMap。默認情況下這兩個變量都是null,只有當前線程調用ThreadLoacl類的setget方法時纔會創建它們,實際上調用這兩個方法時,調用的是ThreadLocalMap類對應的getset方法

ThreadLocalset()方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

總結:

  • 每一個線程都維護一個ThreadLocalMap的引用
  • ThreadLocalMapThreadLocal的內部類,用Entry進行存儲
  • 調用ThreadLocalset()方法時,實際上就是往ThreadLocalMap設置值,key是ThreadLocal對象,值是傳遞過來的泛型變量。
  • 調用ThreadLocalget()方法時,實際上就是往ThreadLocalMap獲取值,key是ThreadLocal對象
  • ThreadLocal本身並不存儲值,它只是作爲一個key來讓線程從ThreadLocalMap獲取value

ThreadLocal 內存泄漏問題

在ThreadLocalMap中,是用key爲ThreadLocal的弱引用,而value是強引用:

static class Entry extends WeakReference<ThreadLocal<?>> {// 所謂弱引引用指的是被弱引用修飾的類
    /** The value associated with this ThreadLocal. */
    Object value; // 普通的對象爲強引用

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

在來垃圾回收的時候,JVM掃描到的所有弱引用對象都會被JVM回收掉,而value是強引用,則不會被清理掉。這樣一來,ThreadLocalMap 中就會出現key爲null的Entry。假如我們不做任何措施的話,value 永遠無法被GC 回收,這個時候就可能會產生內存泄露。但是也不必太過擔心, 因爲設計者已經想到了這點,所以ThreadLocal會自動處理key 爲 null的 value。

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) { // 如果key是null ,則會進行處理
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

使用完 ThreadLocal方法後 最好手動調用remove()方法。

線程池

爲什麼要用線程池

HTTP連接池,數據庫連接池,線程池等等都是使用了池化技術。池化技術的思想就是爲了減少每次獲取資源的消耗,提高對資源的利用率。

線程池提供了一種限制和管理資源(包括執行一個任務)。每個線程池還維護一些基本統計信息,例如已完成任務的數量。

線程池的好處:

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要等待線程的創建就能立即執行。
  • 提供線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

Runnable接口和Callable接口的區別

Runnalbe自Java1.0以來一直存在,但Callable僅在Java1.5中引入,目的是爲了處理Runnable不支持的用例。
Runnable接口不會返回結果或者拋出檢查異常,但是Callable接口可以。所以,如果任務不需要返回結果或拋出異常推薦使用Runnable接口,這樣代碼更加簡潔。

執行execute()方法和submit()的區別

1.execute()方法用於提交不需要返回值的仍無,所以無法判斷任務是否被線程池成功執行。

2.submit()方法用於提交需要返回值的任務。線程池會返回一個Future類型的對象,通過這個Future對象可以判斷任務是否被成功執行,並且可以通過Futureget()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

如何創建線程池

《阿里巴巴Java開發手冊》中強制線程池不允許使用 Executors 去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,避免資源耗盡的風險

Executors 返回線程池對象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor:允許請求的隊列長度爲 Integer.MAX_VALUE , 可能會堆積大量的請求,從而導致OOM。
  • CachedThreadPool 和 ScheduledThreadPool:允許創建的線程數量爲 Integer.MAX_VALUE,可能會創建大量線程,從而導致OOM。

方法一:通過構造方法實現

在這裏插入圖片描述

方式二:通過 Executor框架的工具類Executors來實現,我們可以創建三種類型的ThreadPoolExecutor

  • FixedThreadPool:該方法返回一個固定線程數量的線程池。該線程池中的 線程數量始終不變。當有一個新的仍無提交時,線程池中若有空閒線程,則立即執行。若沒有,則新的任務會被暫存在一個任務隊列中,待有空閒線程時,便處理在任務隊列中的任務。
  • SingleThreadExecutor:方法返回一個只有一個線程的線程池。若多餘一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閒,按先入先出的順序執行隊列中的任務。
  • CachedThreadPool:該方法返回一個可根據實際情況調整線程數量的線程池。線程池的線程數量不去欸但那個,但若有空閒線程可以複用,則會優先使用可複用的線程。若線程均在工作,又有新的任務提交,則會創建新的線程處理人物。所有線程在當前任務執行完畢後,將線程池複用。

ThreadPoolExecutor 類分析

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

ThreadPoolExecutor構造函數重要參數分析

ThreadPoolExecutor3個最重要的參數:

  • corePoolSize : 核心線程數定義了最小可以同時允許的線程數量
  • maximumPoolSize:當隊列中存放的任務達到隊列容量時,當前可以同時允許的線程數量變爲最大線程數。
  • workQueue:當新任務來的時候會先判斷當前允許的線程數是否達到核心線程數,如果達到的話,新任務就會被存放在隊列中。

ThreadPoolExecutor其他常見參數

1、keepAliveTime:當線程池中的線程數量大於corePoolSIze時,如果這時沒有新的任務提交,核心線程外的線程不會立即銷燬,而是等待,直到等待時間超過了keepAliveTime纔會被回收銷燬。

2、unitkeepAliveTime參數的時間單位。

3、threadFactory:executor 創建新線程的時候會用到。

4、handler:飽和策略。當沒有任何空閒線程時執行的策略。

ThreadPoolExecutor 飽和策略:

定義:

如果當先同時運行的線程數量達到最大線程數並且隊列也已經被填滿時,ThreadPoolTaskExecutor 定義一些策略:

  • ThreadPoolExecutor.AbortPolicy : 拋出 RejectedExecutionException來拒絕新任務的處理。
  • ThreadPoolExecutor.CallerRunsPolicy:調用執行自己的線程運行任務。您不會拒絕任務請求。到那時這種策略會降低對於新任務提交速度,影響程序的整體性能。另外,這個策略喜歡增加隊列容量。如果您的應用程序可以承受此延遲並且你不能丟棄任何一個任務請求的話,可以使用這個策略。
  • ThreadPoolExecutor.DiscardPolicy : 不處理新任務,直接丟棄掉。
  • **ThreadPoolExecutor.DiscardOldestPolicy:**此策略將丟棄最早的未處理虧待任務請求。

線程池執行原理

在這裏插入圖片描述
如果我們在代碼中放置了10個任務,我們配置的核心線程數爲5,等待隊列容量爲100,所以每次只可能 存在5個任務同時執行,剩下的5個任務會被存放在等待隊列中,等待當前某個正在執行的任務執行完後,纔會開始執行。

CAS

CAS: Compare And Swap 比較成功並交換。CAS體現的是一種樂觀鎖機制。CAS涉及到三個元素:指定的內存地址,期盼值和目標值。將指定內存地址的值與期盼值相比較,如果成功就將內存地址的值爲目標值。

CAS在Java中的底層實現

CAS在Java中的實現是 juc的atomic包下的Atomicxx原子類。

我們知道普通的自增操作(i++)不是原子性的,但是可以使用AtomicInteger來保證自增的原子性。

public class Test {
    public AtomicInteger i;

    public void add() {
        i.getAndIncrement();
    }
}

我們來看getAndIncrement的內部:

public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}

再深入到getAndAddInt():

@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

這裏我們見到了weakCompareAndSetInt(),它是compareAndSetInt()方法的一個封裝,CAS縮寫的由來compareAndSetInt()(本人jdk版本jdk12,jdk8是CompareAndSwapInt),compareAndSetInt()是一個本地native方法,想要知道怎麼實現的需要去hotspot源碼中查看。

@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
                                          int expected,
                                          int x) {
    return compareAndSetInt(o, offset, expected, x); 
}

先說說getAndAddInt的實現,它會根據當前Atomic的value在內存中地址獲取到當前對象的值,然後再重複此操作,把之前獲得的值與第二遍獲得的值進行比較,如果兩個值相等,就把內存地址的值更新爲新值,否則就自旋相比較。

CAS的缺點

  • 循環時間開銷大:Atomic的CAS並沒有進行CAS失敗的退出處理,只是單純的循環比較,如果長時間自旋會給CPU帶來非常大的執行開銷。
  • 只能保證一個共享變量的原子操作:Atomic原子類只能保證一個變量的原子操作,如果是多數據的話,還是考慮使用互斥鎖來實現數據同步。
  • ABA問題:CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS檢查時會發現它的值並沒有發生變化,但是實際上變量已經變化了。

解決ABA問題

再juc的Atomic包中提供了AtomicStampReference類,這個類較普通的原子累新增了一個stamp字段,它的作用相當於version(版本號)。每次修改這個引用的值,也會修改stamp的值,當發現stamp的值與期盼的stamp值不一樣時,會修改失敗,類似於以version實現樂觀鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章