Spring中多執行緒的使用及問題

李明發表於2022-04-25

由於本週大部分時間都在寫原型,主要遇到的問題就是對實際功能理解不準確導致多次修改原型浪費了很多時間,這也就告訴我們一定要明確實際要求再去下手。
因為之前會議中也多次提到了執行緒,而我本人對執行緒沒有什麼理解於是便有了以下文章。

為什麼使用多執行緒

在我們開發系統過程中,經常會處理一些費時間的任務(如:向資料庫中插入大量資料),這個時候就就需要使用多執行緒。

Springboot中是否對多執行緒方法進行了封裝

是,Spring中可直接由@Async實現多執行緒操作

如何控制執行緒執行中的各項引數

通過配置執行緒池。

執行緒池ThreadPoolExecutor執行規則如下

然後我們來認為構造一個執行緒池來試一下:

@Configuration
@EnableAsync
public class ThreadPoolConfig implements AsyncConfigurer {
  /**
   * 核心執行緒池大小
   */
  private static final int CORE_POOL_SIZE = 3;

  /**
   * 最大可建立的執行緒數
   */
  private static final int MAX_POOL_SIZE = 10;

  /**
   * 佇列最大長度
   */
  private static final int QUEUE_CAPACITY = 10;

  /**
   * 執行緒池維護執行緒所允許的空閒時間
   */
  private static final int KEEP_ALIVE_SECONDS = 300;

  /**
   * 非同步執行方法執行緒池
   *
   * @return
   */
  @Override
  @Bean
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setMaxPoolSize(MAX_POOL_SIZE);
    executor.setCorePoolSize(CORE_POOL_SIZE);
    executor.setQueueCapacity(QUEUE_CAPACITY);
    executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
    executor.setThreadNamePrefix("LiMingTest");
    // 執行緒池對拒絕任務(無執行緒可用)的處理策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
  }
}

ThreadPoolExecutor是JDK中的執行緒池實現,這個類實現了一個執行緒池需要的各個方法,它提供了任務提交、執行緒管理、監控等方法。

corePoolSize:核心執行緒數

執行緒池維護的最小執行緒數量,預設情況下核心執行緒建立後不會被回收(注意:設定allowCoreThreadTimeout=true後,空閒的核心執行緒超過存活時間也會被回收)。

大於核心執行緒數的執行緒,在空閒時間超過keepAliveTime後會被回收。

maximumPoolSize:最大執行緒數

執行緒池允許建立的最大執行緒數量。

當新增一個任務時,核心執行緒數已滿,執行緒池還沒達到最大執行緒數,並且沒有空閒執行緒,工作佇列已滿的情況下,建立一個新執行緒,然後從工作佇列的頭部取出一個任務交由新執行緒來處理,而將剛提交的任務放入工作佇列尾部。

keepAliveTime:空閒執行緒存活時間

當一個可被回收的執行緒的空閒時間大於keepAliveTime,就會被回收。
被回收的執行緒:

設定allowCoreThreadTimeout=true的核心執行緒。
大於核心執行緒數的執行緒(非核心執行緒)。

workQueue:工作佇列

新任務被提交後,如果核心執行緒數已滿則會先新增到工作佇列,任務排程時再從佇列中取出任務。工作佇列實現了BlockingQueue介面。

handler:拒絕策略

當執行緒池執行緒數已滿,並且工作佇列達到限制,新提交的任務使用拒絕策略處理。可以自定義拒絕策略,拒絕策略需要實現RejectedExecutionHandler介面。

JDK預設的拒絕策略有四種:

AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。
DiscardPolicy:丟棄任務,但是不丟擲異常。可能導致無法發現系統的異常狀態。
DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新提交被拒絕的任務。
CallerRunsPolicy:由呼叫執行緒處理該任務。

我們在非測試檔案中直接使用new Thread建立新執行緒時編譯器會發出警告:

不要顯式建立執行緒,請使用執行緒池。
說明:使用執行緒池的好處是減少在建立和銷燬執行緒上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題

public class TestServiceImpl implements TestService {
  private final static Logger logger = LoggerFactory.getLogger(TestServiceImpl.class);
  @Override
  public void task(int i) {
      logger.info("任務: "+i);
  }
}
  @Autowired
  TestService testService;
  @Test
  public void test() {
    for (int i = 0; i < 50; i++) {
      testService.task(i);
    }

我們可以看到一切執行正常;
圖片.png

之後我有對執行緒進行了一些測試:

class TestServiceImplTest {
  @Test
  public void test() {
    Thread add = new AddThread();
    Thread dec = new DecThread();
    add.start();
    dec.start();
    add.join();
    dec.join();
    System.out.println(Counter.count);
  }

  static class Counter {
    public static int count = 0;
  }

  class AddThread extends Thread {
    public void run() {
      for (int i=0; i<10000; i++) { Counter.count += 1; }
    }
  }

  class DecThread extends Thread {
    public void run() {
      for (int i=0; i<10000; i++) { Counter.count -= 1; }
    }
  }

一個自增執行緒,一個自減執行緒,對0進行同樣次數的操作,理應結果仍然為零,但是執行結果卻每次都不同。
經過搜尋之後發現對變數進行讀取和寫入時,結果要正確,必須保證是原子操作。原子操作是指不能被中斷的一個或一系列操作
例如,對於語句: n +=1; 看似只有一行語句卻包括了3條指令:

讀取n, n+1, 儲存n;

比如有以下兩個程式同時對10進行加1操作

這說明多執行緒模型下,要保證邏輯正確,對共享變數進行讀寫時,必須保證一組指令以原子方式執行:即某一個執行緒執行時,其他執行緒必須等待。

 static class Counter {
    public static final Object lock = new Object();//每個執行緒都需獲得鎖才能執行
    public static int count = 0;
  }

  class AddThread extends Thread {
    public void run() {
      for (int i=0; i<10000; i++) {
        synchronized(Counter.lock) { static class Counter {
    public static final Object lock = new Object();
    public static int count = 0;
  }

  class DecThread extends Thread {
    public void run() {
      for (int i=0; i<10000; i++) {
        synchronized(Counter.lock) {
          Counter.count -= 1;
        }
      }
    }
  }

值得注意的是每個類可以設定多個鎖,如果執行緒獲取的不是同一個鎖則無法起到上述功能;

springBoot中也定義了很多型別的鎖,在此就不一一說明了,我們目前能做到的就是注意專案中的非同步操作,觀察操作所使用的執行緒,做到在以後專案中遇到此類問題時能及時發現問題,解決問題。

相關文章