1. 程式人生 > 實用技巧 >【STM32F407】第9章 RL-TCPnet V7.X除錯方法(Event Recorder和串列埠兩種)

【STM32F407】第9章 RL-TCPnet V7.X除錯方法(Event Recorder和串列埠兩種)

在一個應用程式中,我們需要多次使用執行緒,也就意味著,我們需要多次建立並銷燬執行緒。而建立並銷燬執行緒的過程勢必會消耗記憶體。而在Java中,記憶體資源是及其寶貴的,所以,我們就提出了執行緒池的概念。

執行緒池:Java中開闢出了一種管理執行緒的概念,這個概念叫做執行緒池,從概念以及應用場景中,我們可以看出,執行緒池的好處,就是可以方便的管理執行緒,也可以減少記憶體的消耗。

執行緒池的優勢:

執行緒池做的工作主要是控制執行的執行緒的數量,處理過程中將任務放入佇列,然後線上程建立後啟動這些任務,如果執行緒數量超過了最大數量超出數量的執行緒排隊等待,等其它執行緒執行完畢,再從佇列中取出任務來執行。

主要特點:執行緒複用、控制最大併發數、管理執行緒。

1.降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。

2.提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。

3.提高執行緒的課管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

Java中的執行緒池是通過Executor框架來實現的,而我們建立時,一般使用它的子類:ThreadPoolExecutor。

java中提供了一個靜態工廠方法來建立不同的執行緒池:Executors

通過靜態方法創建出的執行緒都實現了ExecutorService介面

常用的方法包括:

FixedThreadPool(int threads):定長的執行緒池,有核心執行緒,核心執行緒的即為最大的執行緒數量,沒有非核心執行緒

SingleThreadPool():只有一條執行緒來執行任務,適用於有順序的任務的應用場景。

CachedThreadPool():可快取的執行緒池,該執行緒池中沒有核心執行緒,非核心執行緒的數量為Integer.max_value,就是無限大,當有需要時建立執行緒來執行任務,沒有需要時回收執行緒,適用於耗時少,任務量大的情況。

SecudleThreadPool:週期性執行任務的執行緒池,按照某種特定的計劃執行執行緒中的任務,有核心執行緒,但也有非核心執行緒,非核心執行緒的大小也為無限大。適用於執行週期性的任務。

WorkStealingPool:java8新增,使用目前機器上可用的處理器作為它的並行級別。

常用的執行緒池建立方法示例如下:

/**
 * 第四種獲取執行緒的方式-執行緒池
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService threadPool=Executors.newFixedThreadPool(5);//一池5個執行緒
        //ExecutorService threadPool=Executors.newSingleThreadExecutor();//一池一個執行緒
        //ExecutorService threadPool=Executors.newCachedThreadPool();//一池N執行緒,根據情況來建立

        try{
            //模擬十個使用者來辦理業務
            for(int i=1;i<=10;i++){
                //submit有返回值
                Future future=threadPool.submit( () -> {
                    System.out.println(Thread.currentThread().getName()+" 辦理業務");
                    return "辦理業務成功";
                });
                System.out.println("獲取結果"+future.get());
                //execute無返回值
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 辦理業務");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

執行緒池的底層就是ThreadPoolExecutor類。

1.Executors.FixedThreadPool(int),執行長期的任務,效能好很多

主要特點如下:

1).建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。

2).newFixedThreadPool建立的執行緒池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue。

2.Executors.SingleThreadPool( ),一個任務一個任務執行的場景

主要特點如下:

1).建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序執行。

2).newSingleThreadExecutor將corePoolSize和maximumPoolSize都設定為1,它使用的LinkedBlockingQueue。

3.Executors.CachedThreadPool( ),適用:執行很多短期非同步的小程式或者負載較輕的伺服器。

主要特點如下:

1).建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。

2).newCachedThreadPool將corePoolSize設定為0,將maximumPoolSize設定為Integer.MAX_VALUE,使用的SynchronousQueue,也就是說來了任務就建立執行緒執行,當執行緒空閒超過60秒,就銷燬執行緒。

七大引數

1.corePoolSize:執行緒池中的常駐核心執行緒數

在建立了執行緒池後,當有請求任務來之後,就會安排池中的執行緒去執行請求任務,近似理解為今日當值執行緒;

當執行緒池中的執行緒數目達到corePoolSize後,就會把到達的任務放到快取隊列當中。

2.maximumPoolSize:執行緒池能夠容納同時執行的最大執行緒數,此值必須大於等於1

3.keepAliveTime:多餘的空閒執行緒的存活時間。當前執行緒池數量超過corePoolSize時,當空閒時間達到keepAliveTime值時,多餘空閒執行緒會被銷燬直到只剩下corePoolSize個執行緒為止。

預設情況下:只有當執行緒池中的執行緒數大於corePoolSize時keepAliveTime才會起作用,直到執行緒池中的執行緒數不大於corePoolSize。

4.unit:keepAliveTime的單位

5.workQueue:任務佇列,被提交單尚未被執行的任務。

6.threadFactory:表示生成執行緒池中工作執行緒的執行緒工廠,用於建立執行緒一般用預設的即可。

7.handler:拒絕策略,表示當佇列滿了並且工作執行緒大於等於執行緒池的最大執行緒數(maximumPoolSize)時如何來拒絕請求執行的runnable的策略。

拒絕策略是當等待佇列也已經排滿了,再也塞不下新任務了,同時,執行緒池中的max執行緒也達到了了,無法繼續為新任務服務。這時候我們就需要拒絕策略機制合理的處理這個問題。

JDK內建的拒絕策略

1)AbortPolicy(預設):直接丟擲RejectedExecutionException異常阻止系統正常執行。

2)CallerRunsPolicy:"呼叫者執行"一種調節機制,該策略既不會拋棄任務,也不會丟擲異常,而是將某些任務回退到呼叫者,從而降低新任務的流量。

3)DiscardOldestPolicy:拋棄佇列中等待最久的任務,然後把當前任務加入佇列中嘗試再次提交當前任務。

4)DiscardPolicy:直接丟棄任務,不予任何處理也不丟擲異常。如果允許任務丟失,這是最好的一種方案。

以上內建拒絕策略均實現了RejectedExecutionHandler介面

執行緒池的底層工作原理

1.在建立了執行緒池後,等待提交過來的任務請求。

2.當呼叫execute()方法新增一個請求任務時,執行緒池會做如下判斷:

1)如果正在執行的執行緒數量小於corePoolSize,那麼馬上建立執行緒執行這個任務;

2)如果正在執行的執行緒數量大於或等於corePoolSize,那麼將這個任務放入佇列;

3)如果這時候佇列滿了且正在執行的執行緒數量還小於maximumPoolSize,那麼還是要建立非核心執行緒立刻執行這個任務;

4)如果佇列滿了且正在執行的執行緒數量大於或等於maximumPoolSize,那麼執行緒池會啟動飽和拒絕策略來執行。

3.當一個執行緒完成任務時,它會從佇列中取下一個任務來執行。

4.當一個執行緒無事可做超過一定的時間(keepAliveTime)時,執行緒池會判斷:

  如果當前執行的執行緒數大於corePoolSize,那麼這個執行緒就被停掉。

  所以執行緒池的所有任務完成後它最終會收縮到corePoolSize的大小。

實際開發中一般手動寫執行緒池,這樣合理利用執行緒資源,避免資源耗盡的。

手寫執行緒池示例:

public class MyThreadPoolDemo {

    public static void main(String[] args) {
        //自定義執行緒池
        ExecutorService threadPool= new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(3),//自定義阻塞佇列長度為3
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()//自定義拒絕策略,預設為AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy,DiscardPolicy
        );

       try{
           //模擬十個使用者來辦理業務,每個使用者都是一個來自外部的請求執行緒
           for(int i=1;i<=10;i++){
               //無返回值
              threadPool.execute(()->{
                   System.out.println(Thread.currentThread().getName()+"\t 辦理業務");
               });
           }
       }catch (Exception e){
           e.printStackTrace();
       }finally {
           //關閉執行緒池
           threadPool.shutdown();
       }
    }  

使用預設的拒絕策略,能夠訪問的上限數=當最大執行緒數(maximumPoolSize) + 任務佇列長度,當超過就會報java.util.concurrent.RejectedExecutionException異常。

死鎖編碼及定位分析

死鎖是指兩個或兩個以上的程序在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力干涉那它們都將無法推進下去。如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。

死鎖示例:

/**
 * 死鎖是指兩個或者兩個以上的程序在執行過程中,
 * 因爭奪資源而造成的一種相互等待的現象,
 * 若無外力干涉,那它們就無法推進下去,從而造成死鎖現象。
 */
public class DeadLockDemo {

    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new DeadLockResource(lockA, lockB), "ThreadA").start();
        new Thread(new DeadLockResource(lockB, lockA), "ThreadB").start();
    }
}

/**
 * 執行緒操作資源類
 */
class DeadLockResource implements Runnable {

    private String lockA;
    private String lockB;

    public DeadLockResource(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "\t 擁有" + lockA + ",試圖獲取" + lockB);
            System.out.println("----------*****************----------");
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t 擁有" + lockB + ",試圖獲取" + lockA);
            }
        }
    }
}

ThreadA持有lockA,試圖獲取lockB,而ThreadB持有lockB,試圖獲取lockA。造成兩個執行緒相互等待,又沒有外力干涉,無法推進下去,最終導致死鎖現象。

產生死鎖的主要原因

1.系統資源不足

2.程序執行推進的順序不合適

3.資源分配不當

死鎖解決

Linux環境下檢視程序:ps -ef | grep java

Windows環境下檢視java程序:jps -l

Windows環境下檢視死鎖棧資訊:jstack +程序號

1.jps命令定位程序號

2.jstack找到死鎖檢視

只有停下程式,找到對應業務程式碼進行修改。

合理配置執行緒池:

首先得知道伺服器cpu核心數

獲取cpu核心數

Runtime.getRuntime().availableProcessors()

業務分為cpu密集型還是IO密集型,根據實際業務來配置

1.CPU密集型

CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速執行。

CPU密集任務只有在真正的多核CPU上才可能得到加速(通過多執行緒),而在單核CPU上,無論你開幾個模擬的多執行緒,該任務都不可能得到加速,因為CPU總的運算能力就那些。

CPU密集型任務配置儘可能少的執行緒數量:

一般公式:CPU核數+1個執行緒的執行緒池

2.IO密集型

1) 由於IO密集型任務執行緒並不是一直在執行任務,則應配置儘可能多的執行緒,如CPU核數*2

2) IO密集型,即該任務需要大量的IO,即大量的阻塞。在單執行緒上執行IO密集型的任務會導致浪費大量的CPU運算能力浪費在等待。所以在IO密集型任務中使用多執行緒可以大大的加速程式執行,即時在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。

IO密集型時,大部分執行緒都阻塞,故需要多配置執行緒數:

參考公式:CPU核數/1-阻塞係數(阻塞係數在0.8~0.9之間),比如8核CPU:8/1-0.9=90個執行緒數