Java執行緒與多執行緒
1.執行緒的定義
1.1概述
1)程序Process:是執行程式的一次執行過程。
2)執行緒Thread:是CPU排程和執行的單位。main()為主執行緒,為系統的主入口,用於執行整個程式。在程式執行的過程中,即使沒有手動建立執行緒,後臺也會有多個執行緒(如主執行緒,gc執行緒)。
3)程序與執行緒的區別:
在一個程序中包含若干個執行緒,但一個程序中至少包含一個執行緒。執行緒的執行由排程器安排排程,其先後順序不能人為干預。
多執行緒就多個執行緒同時執行的過程。
1.2執行緒的狀態
執行緒共有5大狀態,可通過getState()方法獲取當前執行緒的狀態。如下圖
1)新建狀態(New):新建立了一個執行緒物件
2)就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法,就會進入就緒狀態,但此時並不意味著會立即執行
3)執行狀態(Running):就緒狀態的執行緒獲取了CPU使用權,在執行執行緒中的程式碼塊
4)阻塞狀態(Blocked):執行緒因為某種原因放棄CPU使用權(如呼叫sleep,wait或同步鎖定),暫時停止執行。
5)死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。死亡狀態的執行緒無法再次使用,需要重新建立。在程式中需要停止執行緒時,建議使用標誌變數讓執行緒自己停止。
1.3執行緒狀態變化的幾種方式
1)執行緒休眠:sleep(毫秒)
指定當前執行緒阻塞的時間,單位是毫秒。執行完sleep後的執行緒會進入就緒狀態,可用於倒計時及延時等場景。每一個物件都有一個鎖,但sleep不會釋放鎖。
Thread.sleep(2000);
上述程式碼是讓當前執行緒休眠2秒。
2)執行緒禮讓:yield
讓當前正在執行的執行緒暫停,進入就緒狀態,讓CPU重新排程。
Thread.yield();
上述程式碼是讓當前執行緒禮讓,但不一定禮讓成功。原因是此轉入就緒狀態的執行緒也同樣可以再次被CPU進行排程。
3)執行緒插隊:join
當前執行緒A在執行過程中,另一個的執行緒B插隊進來執行,B執行完成後當前執行緒A再繼續執行。
1.4執行緒的優先順序
在Java中提供了一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有執行緒,執行緒排程器按照執行緒的優先順序來排程誰先執行。優先順序使用資料表示,範圍在1~10。
執行緒的最小優先順序是Thread.MIN_PRIORITY=1,最大優先順序是Thread.MAX_PRIORITY=10,預設的優先順序是Thread.NORM_PRIORITY=5。通過getPriority()獲取優先順序,通過setPriority()設定優先順序。
當然不一定優先順序越高就越先執行。如main執行緒的優先順序是5,其會先執行,再按優先順序執行。
2.執行緒的建立方式
2.1 繼承Thread
2.1.1 建立執行緒的步驟
1)自定義類繼承Threadl類
2)重寫run方法,編寫執行緒執行體
3)在main()方法中建立自定義的執行緒物件,呼叫start()方法啟動執行緒
2.1.2 建立的程式碼
package com.zys.example; public class MyThread1 extends Thread { @Override public void run() { for (int i = 1; i <= 200; i++) { System.out.println("我是自定義建立的執行緒1:" + i); } } public static void main(String[] args) { MyThread1 thread1 = new MyThread1(); thread1.start(); for (int i = 1; i <= 1000; i++) { System.out.println("我是主執行緒:" + i); } } }
分析:執行main方法後,觀察列印結果,發現main方法和自定義的執行緒列印的結果是交叉的,說明main方法執行的過程中自定義的執行緒也在執行,也就是說這兩個執行緒是並行交替執行的。
注意:執行緒開啟後不一定會立即執行,而是由CPU排程執行的。
2.2 實現Runnable介面
2.2.1 建立執行緒的步驟
1)自定義類實現Runnable類
2)重寫run方法,編寫執行緒執行體
3)在main()方法中建立執行緒物件,傳入Runnable的實現類物件,呼叫start()方法啟動執行緒
2.2.2 建立的程式碼
package com.zys.example; public class MyThread2 implements Runnable { @Override public void run() { for (int i = 1; i <= 200; i++) { System.out.println("我是自定義建立的執行緒2:" + i); } } public static void main(String[] args) { Thread thread = new Thread(new MyThread2()); thread.start(); for (int i = 1; i <= 1000; i++) { System.out.println("我是主執行緒:" + i); } } }
分析:執行main方法後,觀察列印結果大概和實現Thread類的執行緒列印結果類似。
3.執行緒併發與同步問題
3.1執行緒併發的案例
1)買火車票案例
package com.zys.example; public class MyThread3 implements Runnable { private Integer ticketNums = 10; @Override public void run() { while (ticketNums > 0) { try { //模擬延遲操作,相當於網路延時 Thread.sleep(200); //買票。票數減少 System.out.println(Thread.currentThread().getName() + "買到了第" + ticketNums-- + "張票"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread thread = new Thread(myThread3, "小紅"); Thread thread2 = new Thread(myThread3, "小李"); Thread thread3 = new Thread(myThread3, "小張"); thread.start(); thread2.start(); thread3.start(); } }
執行後會發現,總會有人取到第0張票,這是不符合正常邏輯的。
2)Arraylist加元素案例
package com.zys.example; import java.util.ArrayList; import java.util.List; public class MyThread4 { public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<>(); for (int i = 0; i < 10000; i++) { //動態把執行緒名稱加入集合 new Thread(() -> list.add(Thread.currentThread().getName())).start(); } Thread.sleep(5000); System.out.println(list.size()); } }
此案例是動態建立1萬個執行緒,並把執行緒名稱加入集合。但在執行時,發現集合中的元素個數並不一定都是1萬,也有少於一萬的結果。
3.2問題分析
在多個執行緒同時操作同一個物件時,由於執行緒的執行順序是由CPU決定的,不能為人干預,因此是執行緒不安全的,會造成資料混亂。有效的解決方案就是使用執行緒同步。
3.3執行緒同步
執行緒同步:實際是一種等待機制,多個需要同時訪問同一個物件的執行緒進行此物件的等待池形成佇列並加鎖,當前面的執行緒執行完成釋放此物件鎖,下一個執行緒才能使用,從而保證資料的安全性。
用法很簡單,加入同步塊synchronized(obj){}即可,當然也可以直接對方法鎖定。其中obj是需要被監視的物件,物件的操作放到塊內部。當第一個執行緒訪問時,鎖定同步監視器,執行塊中程式碼;當第二個執行緒訪問時,發現同步監視器被鎖定,無法訪問。當第一個執行緒訪問完畢後第二個執行緒按照執行緒一的方式訪問並鎖定同步監視器。
1)對買票案例加入同步鎖
package com.zys.example; public class MyThread3 implements Runnable { private Integer ticketNums = 10; @Override public void run() { //加入同步鎖 synchronized (ticketNums) { while (ticketNums > 0) { try { //模擬延遲操作,相當於網路延時 Thread.sleep(200); //買票。票數減少 System.out.println(Thread.currentThread().getName() + "買到了第" + ticketNums-- + "張票"); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread thread = new Thread(myThread3, "小紅"); Thread thread2 = new Thread(myThread3, "小李"); Thread thread3 = new Thread(myThread3, "小張"); thread.start(); thread2.start(); thread3.start(); } }
2)買票案例優化(推薦使用)
當然,也可以將鎖加在方法上,對上述程式碼進行改造
package com.zys.example; public class MyThread3 implements Runnable { private Integer ticketNums = 10; //執行緒停止的標誌 private Boolean flag = true; @Override public void run() { while (flag) { buy(); } } //加入同步鎖 private synchronized void buy() { try { if (ticketNums <= 0) { flag = false; return; } //模擬延遲操作,相當於網路延時 Thread.sleep(200); //買票。票數減少 System.out.println(Thread.currentThread().getName() + "買到了第" + ticketNums-- + "張票"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread thread = new Thread(myThread3, "小紅"); Thread thread2 = new Thread(myThread3, "小李"); Thread thread3 = new Thread(myThread3, "小張"); thread.start(); thread2.start(); thread3.start(); } }
將買票的動作抽成一個方法,對此方法進行上鎖。另外,使用了自定義的標誌來終止執行緒,從而保證了執行緒的高效性。
3)對ArrayList進行同步鎖
package com.zys.example; import java.util.ArrayList; import java.util.List; public class MyThread4 { public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<>(); for (int i = 0; i < 10000; i++) { //動態把執行緒名稱加入集合 new Thread(() -> { synchronized (list) { list.add(Thread.currentThread().getName()); } }).start(); } Thread.sleep(5000); System.out.println(list.size()); } }
ArrayList本身就是不安全的,故多個執行緒對其操作時,必填使用同步鎖。
3.4 Lock鎖
JDK也提供了一種執行緒同步機制,顯示的定義同步鎖物件來實現同步。使用ReentrantLock(其實現了Lock介面)對物件加鎖,和synchronized具有相同的併發性。
對上述的買票案例進行顯示加鎖:
package com.zys.example; import java.util.concurrent.locks.ReentrantLock; public class MyThread3 implements Runnable { private Integer ticketNums = 10; //執行緒停止的標誌 private Boolean flag = true; //定義鎖 private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (flag) { buy(); } } private void buy() { try { //加鎖 lock.lock(); if (ticketNums <= 0) { flag = false; return; } //模擬延遲操作,相當於網路延時 Thread.sleep(200); //買票。票數減少 System.out.println(Thread.currentThread().getName() + "買到了第" + ticketNums-- + "張票"); } catch (InterruptedException e) { e.printStackTrace(); } finally { //釋放鎖,一般均在finally中釋放 lock.unlock(); } } public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread thread = new Thread(myThread3, "小紅"); Thread thread2 = new Thread(myThread3, "小李"); Thread thread3 = new Thread(myThread3, "小張"); thread.start(); thread2.start(); thread3.start(); } }
先定義鎖,然後在需要操作的物件前加鎖,操作完成後釋放鎖。
4.執行緒池
在使用多個執行緒時,頻繁的建立、銷燬執行緒,其實是比較耗費資源的,那麼便可以提前建立好多個執行緒,放入一個池子中,使用時直接從池中獲取,使用完畢後再放入池中,實現重複利用,節省資源。
4.1在SpringBoot中使用執行緒池
1)執行緒池配置
使用執行緒池時,需先配置執行緒池的主要引數。同時也需要添加註解 @EnableAsync 來開啟執行緒池非同步
package com.zys.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; /** * @Auther: zxh * @Date: 2022/3/20 09:17 * @Description: 配置執行緒池 */ @Configuration @EnableAsync public class TaskExecutePool { //核心執行緒數 private static final Integer CORE_POOL_SIZE = 20; //最大執行緒數 private static final Integer MAX_POOL_SIZE = 20; //快取佇列容量 private static final Integer QUEUE_CAPACITY = 200; //執行緒活躍時間(秒) private static final Integer KEEP_ALIVE = 60; //預設執行緒名稱字首 private static final String THREAD_NAME_PREFIX = "MyExecutor-"; @Bean("MyAsyncTaskExecutor") public Executor myTaskAsyncPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(CORE_POOL_SIZE); executor.setMaxPoolSize(MAX_POOL_SIZE); executor.setQueueCapacity(QUEUE_CAPACITY); executor.setKeepAliveSeconds(KEEP_ALIVE); executor.setThreadNamePrefix(THREAD_NAME_PREFIX); //拒絕策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
2)使用執行緒池
在需要使用執行緒池的方法上加上註解@Async,但需要指定執行緒池注入Spring時Bean的名稱。
先建立介面,用於訪問:
package com.zys.example.controller; import com.zys.example.service.TestService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") @Slf4j public class TestController { @Autowired private TestService testService; @GetMapping("/test") public void test() { for (int i = 0; i < 100; i++) { testService.test(i); } } }
建立實現類,使用執行緒池:
package com.zys.example.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service @Slf4j public class TestService { @Async("MyAsyncTaskExecutor") public void test(Integer i) { if (i % 2 == 0) { log.info("是偶數"); } else { log.info("是奇數"); } } }
此介面及實現類的作用是使用執行緒池判斷100內的數字是奇數還是偶數。
需要注意的是,在使用@Async註解時,需要指定配置執行緒池時注入Spring的Bean名稱,否則不會使用配置的執行緒池。
另外,呼叫此註解修飾的方法時,必須在另一個類中呼叫,不能在宣告此方法的類中呼叫,否則此註解不生效。
3)測試
啟動專案,訪問http://localhost:8080/api/test,即可看到控制檯列印的結果,其中執行緒的名稱是配置類中定義的。