Java併發 總結

併發與並行

併發與並行在日常開發中經常被提到,很容易混淆,但一字之差,意思卻大不相同。

  • 併發一般指的是在指定時間窗口內處理請求的數量;
  • 並行指的是在同一時刻同時執行的任務數;

可以從實現機制上來理解併發和並行,併發是通過cpu在不同的線程之間切換時間片來實現的,也就是說在單核cpu下也可以實現併發,而並行則是通過cpu多核技術實現的,由於每個cpu核心在同一時刻只能運行一個線程,引入多核後,同一時刻就可以在不同的cpu核心上同時執行多個線程。

JMM(Java內存模型)

這部分內容比較抽象,小編儘量從自己的理解觸發,把這個問題說清楚。

共享資源的線程安全問題是併發編程中面臨的最重要的問題之一,小編覺得,共享資源的線程安全性可以從三個方面去看,分別是:

  • 原子性:類似於事務,某些操作不允許有中間狀態,主要挑戰來源於cpu時間片的輪轉;
  • 可見性:一個線程對數據的寫要對另一線程可見,主要挑戰來源於線程本地緩存;
  • 有序性:保證代碼執行的有序性,主要挑戰來源於Java編譯器和cpu的亂序執行優化;

以上三點是我們進行多線程編程時,需要面對的主要問題。在具體聊JMM前,需要先對MESI緩存一致性協議有一個簡單的瞭解。

MESI的全稱是Modified-Exclusive-Shared-Invalid的簡寫,它是一種緩存一致性協議,主要用來解決cpu緩存與主內存之間的數據不一致的問題,下圖是計算機cpu、緩存、和內存的一個抽象結構:
在這裏插入圖片描述
由於CPU運算速度和主內存的讀寫性能之間存在着巨大的差異,所以在cpu中引入多級緩存來避免cpu提供數據的IO速度,避免cpu由於主內的性能問題導致停擺,但由於每個cpu都有自己的緩存,如何保證這些cpu緩存數據的一致性 避免髒讀就成了第一道攔路虎,爲此,偉大的人類提出了MESI緩存一致性協議,下面以一個簡單的例子來說明一下啥叫MESI。

在這裏插入圖片描述
假如有兩個線程A和B對主內存中的同一個遍歷x執行 x+=1操作,當線程A從主內存中讀取了x後,總線上x的狀態爲E(Exclusive),當線程B也從主內存中讀取x時,總線上x的狀態變成了S(Shared),當線程A向主內存寫入計算後的x值時,總線上x的狀態變成了M(Modified),寫入完成後,總線上x的狀態變爲I(Invlid),最終的這個狀態I,會導致B線程所在的cpu緩存中x的值失效,最終內存模型的抽象就編程了這樣

在這裏插入圖片描述
在主內存和cpu緩存之間多了一層MESI協議,以小編的理解,MESI協議主要解決的是內存可見性問題,對於併發編程中的另外兩個問題-原子性和順序性卻無能爲力。

MESI協議畢竟是協議,並不是具體的實現,Java爲了解決這些併發編程中的問題,就在MESI的基礎上提出了JMM,即Java Memory Model - Java內存模型,小編自己理解MESI和JMM的關係是這樣的:

class abstract MESI {
	abstract void 原子性();
	abstract void 順序性();
	void 可見性() {
		吧啦吧啦
	}
}

class JMM extends MESI {
	void 原子性() {
		吧啦吧啦
	}
	void 順序性() {
		吧啦吧啦
	}
}

一切看起來都很美好,在JMM的保駕護航下,你覺得可以盡情的遨遊在併發編程的海洋裏,但是現實總喜歡打臉,JMM它說白了也是一個規範,這些規範就是一些列的happens-before規則,也就是說JMM和MESI都是隻會說不會幹的傢伙,具體的happens-before規則如下:

  1. 程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作;
  2. 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖;
  3. volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀;
  4. 傳遞性規則:A happens-before B,B happens-before C,那麼A happens-before C;
  5. start()規則:A線程的B.start() happens-before與B線程的任意操作;
  6. join()規則:A.join(B),那麼B的任意操作 happens-before與A的後續所有操作;

那麼,這些規則在Java中如何實現的呢?體現到語言層面,就是我們經常用到的volatile、synchronized、final、Lock以及concurrent包下的各種併發組件,利用這些腳手架,Java程序員就可以實現併發安全的程序。

這些併發編程腳手架都有自己不同的實現原理,例如volatile可以保證內存可見性和禁用重排序優化,它是通過在指令中加入#lock標記實現的,synchronized可以保證順序性、可見性和原子性,底層利用monitorenter和monitorexist指令實現,final提供不可變性保證(不可變性也實現併發安全的一種方式,例如經典的併發框架akka就是利用消息的不可變性實現了簡單是併發模型),而Lock底層基於AQS在語言層面實現了鎖的語義。

以上就是小編對JMM的理解,包含了從MESI協議到最終的Java併發編程腳手架的演進過程。

Java線程基礎

什麼是線程

在計算機中,進程是資源分配的最小單位,一個進程擁有獨立的內存空間,在線程內部,包含了多個線程,線程是計算機運行的最小單元。

多線程編程的收益與代價

隨着摩爾定理逐漸走向失效,計算機在縱向的性能提升越來越困難,進而就發展出了多cpu多核處理器技術,通過並行儘可能的壓榨計算機性能就成了有效的方法。Java原生支持多線程,在日常開發中也經常被用到,使用多線程可以代理很多好處,比如:

  • 簡化編程模型
  • 加快響應時間
  • 利用更多的計算資源

但是,凡事都有兩面性,多線程帶來好處的同時,也會引發很多問題,比如:

  • 共享資源的線程安全問題;
  • 產生死鎖;
  • 性能提升受限於系統資源;

線程優先級

在java中,線程優先級被分爲從1~10級別,默認級別爲5,理論上,線程優先級越大,分配到cpu時間片的概率就越大,但是這並不是一定的,這取決於操作系統的實現,在有些操作系統中,甚至世界忽略了線程的優先級,我們在編寫併發程序時,不能依賴於線程優先級,因爲這是不確定的。

線程的狀態

一個線程的狀態可以分爲以下幾種

  • 初始態
  • 運行
  • 阻塞
  • 等待
  • 超時等待
  • 結束
    在這裏插入圖片描述

Daemon線程

Daemon線程是一張後臺線程,它依附於啓動它的主線程,如果主線程結束,那麼Daemon線程會立即停止運行。可通過Thread.setDaemon(true);來設置一個線程爲Daemon線程。

另外,在適應Daemon線程是,需要注意的是,不能依賴於try{}finally{}做任何事情,因爲當主線程結束後,Daemon線程會被立即終止。

線程啓動

new Thread(Runnable).start();

線程中斷

線程中斷是一種線程結束的途徑,與線程中斷的方法有三個,分別是:

  • interrupt():修改線程中斷表示;
  • isInterrupted():當調用interrupt()方法時,該方法返回true;
  • interrupted():Thread.interrupted(),用來重置當前線程的中斷狀態;

線程中斷結束的正確範式:


class MyTask implements Runnable {
	volatile boolean flag;
	public void run() {
			while(!flag && !isInterrupted()) {
					...
			}
	}
	public vodi setFlag(boolean flag) {
		this.flag = flag;
	}
}

線程間通信

線程之間通信的方式有兩種,一種是共享變量,一種是消息傳遞,其中共享變量時最常用的一種,通常利用synchronized + wait()/notify()/notifyAll()來實現線程間的協作,這種實現線程間通信的編程範式如下:

synchronized(monitorObj) {
	while(condition != true) {
		wait();
	}
	do something;
}

synchronized(monitorObj) {
	do something();
	monitorObj.notifyAll();
}

ThreadLocal

ThreadLocal是一種實現線程安全的常用方案,它被稱爲線程封閉,可以爲每個線程存儲數據自己的數據。ThreadLocal底層實際上就是一個Map<Thread,Value>結構,但他的應用卻非常廣泛,很多時候也被用於在一個業務處理流程中的各個不同階段共享遍歷,比如用戶登錄信息等。

Synchronized原理

synchronized是java中最早的解決多線程同步的方式,它可以修飾靜態方法、成員方法和同步代碼塊,其中修飾靜態方法時用到的鎖對象時類的Class對象,修飾同步方法時用到的鎖對象是當前對象,同步代碼塊中用到的鎖對象時在代碼塊中制定的鎖對象。

synchronized在早期是一個重量級鎖,加鎖和解鎖效率都比較低,但是在jdk 1.6以後,對synchronized進行了優化,引入了偏向鎖、輕量級鎖,在鎖競爭不激烈的情況下,可以很大程度上的提高鎖的加鎖和解鎖效率。

鎖的狀態是由jvm自動判斷的,當只有一個線程調用被synchronized修飾的方法時,就會應用偏向鎖,在對象頭中設置當前鎖所偏向的線程id,當這個線程再次獲取鎖時,並不會真正加鎖,而是判斷偏向線程的id是否爲當前線程。當有多個線程存在競爭時,偏向鎖會升級爲輕量級鎖,輕量級鎖是用cas的方法將鎖對象頭設置爲當前線程的mark world,如果設置成功則成功獲取鎖,偏向鎖會出現在存在競爭但在很短的時間內就可以從cas循環中退出,即成功獲取到鎖的場景,主要針對於短小任務,如果競爭情況進一步加劇,synchronized就會升級爲重量級鎖。

AQS 同步工具

AQS是AbstractQueuedSynchronizer的簡稱,它是Java提供的一個底層同步工具類,使用int類型的變量去表示同步狀態,並通過一些列的cas操作來線程安全的管理這個同步狀態,AQS的主要作用是爲Java中的同步組件提供統一的底層支持,例如ReentrantLock、CountdownLatch等。

AQS中包含了兩種隊列:一種是同步隊列,一種是等待隊列。同步隊列用來實現線程同步,等待隊列用於實現等待通知機制。
在這裏插入圖片描述
在這裏插入圖片描述
同步隊列主要用來存儲獲取鎖獲取失敗的線程,它包含了head節點和tail節點,其中head節點指向了當前已經成功獲取同步狀態的線程,當它執行完成後,會利用LockSupport.park()方法喚醒後面正在等待的節點,當線程獲取同步狀態失敗時,會被構造成Node節點添加到線程的末尾。
在這裏插入圖片描述
同步隊列的作用是實現等待通知機制,對應了Lock.Condition對象上的await()和signal()方法,一個成功獲取鎖的線程調用await方法時,會將其從同步隊列的頭結點的位置移動到等待隊列的尾節點,並喚醒他後面的節點,此時,會釋放同步狀態。當調用signal方法時,會將等待隊列中的頭節點移動到同步隊列的尾部,此處並沒有用到cas,因爲能調用signal的方法的肯定是成功獲取了鎖的線程。

JUC中各種鎖的使用及原理

JUC中的很多鎖實現以及併發工具類都是基於AQS來實現的,下面就來總結一下各種鎖的實現原理。JUC中的鎖基本上可以分爲四類:

  • 獨佔鎖
  • 共享鎖
  • 讀寫鎖
  • 可中斷鎖
  • 超時鎖

其中,可中斷鎖和超時鎖是上面三種鎖都具有的兩個特性,下面我們一一來看。

首先來看獨佔鎖的獲取和釋放原理。

class NonfairSync extends Sync { 
	final void lock() {
	        /**
				非公平模型下,一進來就會搶佔鎖狀態
			*/
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            //如果搶佔失敗,則調用acquire方法
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
}
public final void acquire(int arg) {
/**
	這裏做了三件事
	1.嘗試通過模板方法獲取鎖狀態
	2.獲取失敗,則構造一個Node節點,並利用cas的方式線程安全的將其添加到同步隊列末尾
	3.讓被構造成Node節點的線程進入自旋狀態
*/
if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //這裏實現了可重入的邏輯,如果是同一個線程返回調用lock
            //那麼就會增加同步狀態的值
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
 }
  • 鎖的獲取
  1. 調用AQS的acquire()方法;
  2. 調用模板方法tryAcquire(args)嘗試獲取同步狀態;
  3. 如果獲取失敗,則將當前線程構造成一個Node節點;
  4. 利用cas線程安全的把這個節點添加到同步隊列末尾;
  5. 讓該線程進入自旋轉狀態,等到被前驅節點喚醒後,再嘗試獲取同步狀態;
public final boolean release(int arg) {
        //調用模板方法釋放同步狀態
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

protected final boolean tryRelease(int releases) {
            //釋放同步狀態
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //這裏目的是爲了實現重入鎖的釋放,只用同步狀態爲0時,才表示釋放鎖成功
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
  • 鎖的釋放
  1. 調用release()方法;
  2. 調用模板方法tryRelease()是否同步狀態,知道同步狀態爲0時表示釋放成功;
  3. 獲取當前節點的next節點,並利用LockSupport.unpark()方法喚醒該線程;
  4. next節點被喚醒後,判斷它的prev節點是否爲head節點並且是否能夠獲取同步狀態;
  5. 如果都成立,則獲取鎖成功,如果獲取失敗,則利用LockSupport.park()重新進入等待狀態;

下面看一下共享鎖的獲取和釋放流程。

protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                //嘗試獲取同步狀態
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
}
public final void acquireShared(int arg) {
		//如果同步狀態小於0了,說明資源以及被獲取殆盡
        if (tryAcquireShared(arg) < 0)
        
            doAcquireShared(arg);
    }
private void doAcquireShared(int arg) {
		//構造節點
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋
            for (;;) {
            	//獲取當前節點的前一個節點
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //如果當前節點是head節點並且成功獲取了同步狀態
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //否則進入阻塞狀態
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }    
  • 鎖的獲取
  1. 調用acquireShared()方法;
  2. 調用tryAcquireShared()模板方法嘗試獲取同步狀態;
  3. 如果返回值>=0,說明還有同步資源可以獲取,成功獲取鎖
  4. 如果返回值<0,說明同步資源都被佔用了,這是當前線程會被構造成節點加入到同步隊列尾部
  5. 它的prev節點調用LockSupport.unpark()將其喚醒後,會判斷它的前一個節點是否爲head並且是否能夠獲取同步狀態
  6. 如果是,則說明獲取鎖成功,將自己設置爲頭結點
  7. 如果否,則再次利用LockSupport.park掛起自己

下面來看看讀寫鎖的實現原理和鎖獲取及釋放流程:
該讀寫鎖到實現原理是:將同步變量state按照高16位和低16位進行拆分,高16位表示讀鎖,低16位表示寫鎖
在這裏插入圖片描述

  • 寫鎖的獲取與釋放
protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
  1. 獲取同步狀態,並從中分離出低16爲的寫鎖狀態
  2. 如果同步狀態不爲0,說明存在讀鎖或寫鎖
  3. 如果存在讀鎖(c !=0 && w == 0),則不能獲取寫鎖(保證寫對讀的可見性)
  4. 如果當前線程不是上次獲取寫鎖的線程,則不能獲取寫鎖(寫鎖爲獨佔鎖)
  5. 如果以上判斷均通過,則在低16爲寫鎖同步狀態上利用CAS進行修改(增加寫鎖同步狀態,實現可重入)
  6. 將當前線程設置爲寫鎖的獲取線程
  • 寫鎖的釋放
protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

在釋放的過程中,不斷減少讀鎖同步狀態,只爲同步狀態爲0時,寫鎖完全釋放。

  • 讀鎖的獲取和釋放
    讀鎖是一個共享鎖,獲取讀鎖的步驟如下
  1. 取當前同步狀態;
  2. 計算高16爲讀鎖狀態+1後的值;
  3. 如果大於能夠獲取到的讀鎖的最大值,則拋出異常;
  4. 如果存在寫鎖並且當前線程不是寫鎖的獲取者,則獲取讀鎖失敗;
  5. 如果上述判斷都通過,則利用CAS重新設置讀鎖的同步狀態;

讀鎖的獲取步驟與寫鎖類似,即不斷的釋放寫鎖狀態,直到爲0時,表示沒有線程獲取讀鎖。

下面,以超時獨佔鎖爲例來說明超時鎖的實現原理。

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
         //這裏計算了一下超時的截止時間   
        final long deadline = System.nanoTime() + nanosTimeout;
        //構造節點
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
           //進入自旋
            for (;;) {
                //這裏是喚醒後的邏輯,根獨佔鎖一樣
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                //這裏計算剩餘時間
                nanosTimeout = deadline - System.nanoTime();
                //如果已經超時了,直接返回false
                if (nanosTimeout <= 0L)
                    return false;
                //如果剩餘時間 > 1000ns,則再次掛起,否則,進行自旋等待,主要爲了
                //避免由於線程的掛起和喚醒導致超時時間不準
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

最後在看看可中斷鎖的實現邏輯。正常情況下,處在自旋等待中的線程,對其調用interrupt方法並不會真正中斷它的等待狀態

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                //在這裏,當自旋狀態被喚醒後,會檢查當前線程的同步狀態
                //如果Thread.interrupted()返回true,那麼久拋出中斷異常
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
 }

併發集合

ConcurrentHashMap、ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue、LinkedTransforQueue。

線程池原理

線程池可以說是JUC中最常用的組件之一,因爲在日常開發有,有很多需要異步和多線程處理的場景。線程池的原理相對於AQS來說要簡單很多,下面我們就來看一下吧。
在這裏插入圖片描述
這張圖摘自《Java併發編程的藝術》,描述了juc中線程池的原理,實際上,理解線程池原理值需要弄清楚ThreadPoolExecutor構造函數中的幾個參數就可以了:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) { 
}

這是ThreadPoolExecutor的核心構造函數,下面分別解釋一下這些參數的含義:

  • corePoolSize:核心線程數,表示線程池中長期活躍的線程的數量;
  • maximumPoolSize:最大線程數,線程池中能夠啓動的最大線程數;
  • keepAliveTime/unit:這兩個參數組合在一起定義了(maximumPoolSize - corePoolSize)這部分線程的最大空閒時間,如果超過這個時間仍然沒有新任務,則線程資源就會被回收;
  • workQueue:等待隊列,當活躍線程數達到核心線程數並都處於工作狀態時,新進任務就會被添加到等待隊列;
  • threadFactory:線程工廠,用於創建線程池中的線程,一般通過自定義線程工廠來定義線程的名稱;
  • handler:拒絕策略,當線程書已經達到最大線程數時,就會觸發拒絕策略,保證系統資源背會被耗盡;

瞭解了這幾個核心參數的作用實際上也就基本清楚了線程池的工作原理了,配合上面的流程圖,再來描述一下線程的的工作原理:首先,我們通過submit或execute方法向線程池提交任務,如果當前線程數沒有達到核心線程數,就會新啓動一個線程來執行,不管其他線程是否處於空閒狀態(這其實是爲了儘快對線程池進行預熱,使其進入工作狀態),如果以及達到核心線程數,並且沒有空閒線程,那麼新進任務就會被扔到等待隊列中,當核心線程中有空閒的線程了,就會從等待隊列中獲取任務並執行,但是,如果等待隊列也以及達到最大上限了,那麼就會再啓動臨時線程想,相當於僱一些臨時工,來執行這些任務,執行完成之後,如果在指定時間內,沒有進行的任務過來,這些臨時工就會被解僱,也就是銷燬掉,但如果線程數已經達到了最大上限,那麼就會觸發拒絕策略,來保護系統資源,避免無限創建線程,耗盡系統資源,有以下幾種拒絕策略可供選擇:

  • AbortPolicy:直接拋出異常;
  • CallerRunsPolicy:甩給提交任務的線程區執行;
  • DiscardOldestPolicy:丟掉隊列中最早被提交的任務;
  • DiscardPolicy:直接丟掉不處理;

具體選擇哪種策略還需要根據不同的場景進行選擇。

同時,juc還提供另一個Executor框架,提供了一些默認的針對不同場景的線程池實現,比如:

  • FixedThreadPool:可重用固定線程數線程池;
  • SingleThreadPool:只有一個線程的線程池;
  • CachedThreadPool:可根據需要自動創建線程的線程池(maxPoolSize=Integer.MAX_VALUE);
  • ScheduledThreadPoolExecutor:用於執行定時任務的線程池;

其中,前三種雖然看起來比較方便,但是日常工作中儘量不要使用,在阿里編碼規約中也明確禁止使用,原因在於,這三種線程池內部都使用了無界阻塞隊列LinkedBlockingQueue,隊列長度沒有限制,極易出現大量任務堆積最終造成內存溢出的問題,一般,我們在使用線程池的時候回自己指定線程池的核心參數,例如:

  • 根據需求來確定核心線程數和最大線程樹;
  • 自定義線程工廠,給線程指定有意義的名稱,方便查問題排查故障;
  • 自定義實現拒絕策略,或記錄日誌或進行補償處理;

ScheduledThreadPoolExecutor在一些定時任務的場景會替代傳統的java.util.Timer,ScheduledThreadPoolExecutor解決了Timer的一些弊端,比如異常會導致Timer運行中斷等。

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