Tread多線程 什麼是線程? 線程(Thread)是一個程式內部的一條執行流程。 程式中如果只有一條執行流程,那這個程式就是單線程的程式。 多線程是什麼? 多線程是指從軟硬體上實現的多條執行流程的技術(多條線程由cpu負責調度執行)。 多線程的創建方式 方式一:繼承Thread ①定義一個子類My ...
Tread多線程
什麼是線程?
-
線程(Thread)是一個程式內部的一條執行流程。
-
程式中如果只有一條執行流程,那這個程式就是單線程的程式。
多線程是什麼?
多線程是指從軟硬體上實現的多條執行流程的技術(多條線程由cpu負責調度執行)。
多線程的創建方式
方式一:繼承Thread
①定義一個子類MyThread繼承線程類java.lang.Thread,重寫run()方法
②創建MyThread類的對象
③調用線程對象的start()方法啟動線程(啟動後還是執行run方法的)
//(1)讓自定義的MyThread繼承Thread線程類【自定義的類也就具備線程的特性】 public class MyThread extends Thread { //(2)想要聲明自定義的線程執行的時候到底執行什麼代碼,主動重寫父類的run方法 @Override public void run() { for (int i = 1; i <= 20; i++) { System.out.println("【自定義線程】的run方法執行了第" + i + "次!"); } } } public class ThreadTest1 { public static void main(String[] args) { //(3)創建自定義線程類對象並調用start方法啟動線程 MyThread myThread = new MyThread(); myThread.start(); //補:在自定義線程啟動之後,繼續編寫代碼讓主線程執行 for (int i = 1; i <= 20; i++) { System.out.println("【主線程】的run方法執行了第" + i + "次!"); } } }
方式一優缺點
-
優點:編碼簡單
-
缺點:線程類已經繼承Thread,無法繼承其他類,不利於功能的擴展。
方式二:實現Runnable介面
①定義一個線程任務類MyRunnable實現Runnable介面,重寫run()方法
②創建MyRunnable任務對象
③把MyRunnable任務對象交給Thread處理。
public class ThreadTest2 { public static void main(String[] args) { //(3)創建MyRunnable線程任務對象 【和線程還沒有關係】 MyRunnable myRunnable = new MyRunnable(); //(4)創建Thread線程對象,並且線程任務作為構造方法的參數傳遞【槍:√ 彈夾:√】 Thread t = new Thread(myRunnable); t.start(); //補:在自定義線程啟動之後,繼續編寫代碼讓主線程執行 for (int i = 1; i <= 20; i++) { System.out.println("【主線程】的run方法執行了第" + i + "次!"); } } }
方式二優缺點:
-
優點:任務類只是實現介面,可以繼續繼承其他類,實現其他介面,擴展性強。
-
缺點:需要多一個Runnable對象。
前兩種線程創建文件都存在的一個問題
假如線程執行完畢後有一些數據需要返回,他們重寫的run方法均不能直接返回結果。
解決(多線程的第三種創建方式)
利用Callable,FutureTask類型實現。
①創建任務對象
定義一個類實現Callable介面,重寫call方法,封裝要做的事情和要返回的數據。
把Callable類型的對象封裝成FutureTask對象(線程任務對象)。
②把線程任務對象封裝成Thread對象。
③調用Thread對象的start方法啟動線程。
④線程執行完畢後,通過FutureTask對象的的get方法去獲取線程任務執行的結果。
public class ThreadTest4 { public static void main(String[] args) throws ExecutionException, InterruptedException { //(3)Thread類不支持直接傳遞一個Callable線程任務對象【封裝FutureTask對象並且將Callable線程任務作為構造參數傳遞】 MyCallable myCallable = new MyCallable(); FutureTask<Integer> futureTask = new FutureTask<>(myCallable); //(4)創建Thread類對象並且將FutureTask作為參數傳遞 Thread t = new Thread(futureTask); t.start(); //★(5)通過futureTask對象獲取結果 Integer result = futureTask.get(); System.out.println("帶有返回值的線程任務執行完成後返回的結果是:" + result); //補:在自定義線程啟動之後,繼續編寫代碼讓主線程執行 for (int i = 1; i <= 20; i++) { System.out.println("【主線程】的run方法執行了第" + i + "次!"); } } }
方式三優缺點:
-
優點:線程任務類只是實現介面,可以繼續繼承類和實現介面,擴展性強;可以線上程執行完畢後去獲取線程執行的結果。
-
缺點:編碼複雜一點。
三種線程創建方法比較
Thread的常見用法
public void run() 線程的任務方法
public void start() 啟動線程
public Strign getName() 獲取1當前線程名稱,線程名稱預設是Thread-索引
public void setName(String name) 為線程設置名稱
public static Thread currentThread() 獲取當前執行的線程對象
public static void sleep(long time) 讓當前執行的線程休眠多少毫秒後,再繼續執行
public void join() 讓調用這個方法的線程先執行完
public class MyRunRunable implements Runnable { @Override public void run() { for (int i = 1; i <= 200; i++) { //在列印的時候,想要【獲取到執行當前這行代碼的線程】的線程名稱 //通過★Thread.currentThread():獲取當前執行此方法的線程對象 System.out.println(Thread.currentThread().getName() + "已經跑了" + i + "米!"); } } } public class ThreadTest5 { public static void main(String[] args) throws ExecutionException, InterruptedException { String threadName = Thread.currentThread().getName(); System.out.println("【主線程名稱】:" + threadName); MyRunRunable myRunRunable = new MyRunRunable(); //線程起名方式(1):通過線程對象調用setName方法傳遞名稱 Thread t1 = new Thread(myRunRunable); t1.setName("張二狗"); //思考:模擬兩個人跑 => 兩個線程跑【跑的邏輯一樣 所以使用同一個線程任務】不會幹擾【底層:線程棧 線程執行過程中產生的變數數據都線上程棧中保存】 //線程起名方式(2):通過new Thread構造方法的時候,將參數一作為線程任務,參數二作為線程名稱 Thread t2 = new Thread(myRunRunable, "劉鐵柱"); t1.start(); //t1.join(); 【讓調用此方法的線程先執行完:插隊】 t2.start(); } }
線程安全
什麼是線程安全問題?
多個線程,同時操作同一個共用資源的時候,可能會出現業務安全問題。
取錢的線程安全問題
場景:小明和小紅是一對夫妻,他們有一個共同的賬戶,餘額是10萬元,如果小明和小紅同時來取錢,並且2人各自都在取錢10萬元,可能會出現什麼問題呢?
線程安全問題出現的原因?
-
存在多個線程在同時執行
-
多個線程同時訪問一個共用資源
-
存在修改共用資源的情況
定義一個賬號類
package com.itheima.safe; import java.time.LocalTime; public class Account { private String accoundId; private Integer money; //取錢:takeMoney public void takeMoney(Integer money) { //獲取當前取錢的線程名稱 String name = Thread.currentThread().getName(); System.out.println(LocalTime.now() + " " + name + "準備開始取錢!"); if (this.money >= money) { System.out.println(LocalTime.now() + " " + name + "取出了" + money + "元!"); this.money -= money; } else { System.out.println(LocalTime.now() + " " + name + "餘額不足!"); } System.out.println(LocalTime.now() + " 賬戶的餘額是:" + this.money + "元!"); } public String getAccoundId() { return accoundId; } public void setAccoundId(String accoundId) { this.accoundId = accoundId; } public Integer getMoney() { return money; } public void setMoney(Integer money) { this.money = money; } public Account() { } public Account(String accoundId, Integer money) { this.accoundId = accoundId; this.money = money; } }
定義線程類
public class TakeMoneyRunnable implements Runnable { //線程任務需要訪問到Account賬戶對象【將賬戶對象作為線程任務的構造方法 並且只給出一個有參構造】 private Account account; public TakeMoneyRunnable(Account account) { this.account = account; } @Override public void run() { account.takeMoney(100000); } }
測試類
package com.itheima.safe; public class TakeMoneyThreadTest { public static void main(String[] args) { Account account = new Account("CHINA-BANK-62261728738", 100000); //創建線程任務【由於兩個線程的邏輯一樣 只需要一個線程任務】 TakeMoneyRunnable takeMoneyRunnable = new TakeMoneyRunnable(account); //創建線程對象並且傳遞線程任務和線程名稱 Thread t1 = new Thread(takeMoneyRunnable, "張二狗"); Thread t2 = new Thread(takeMoneyRunnable, "王美麗"); t1.start(); t2.start(); } }
線程同步(解決線程安全)
線程同步的思想
讓多個線程實現先後依次訪問共用資源,這樣就解決了安全問題。
同步代碼塊
作用:把訪問共用資源的核心代碼給上鎖,以此保證線程安全。
原理:每次只允許一個線程加鎖後進入,執行完畢後自動解鎖,其他線程次才可以來執行。
對線程安全改造
public void takeMoney(Integer money) { String name = Thread.currentThread().getName(); System.out.println(LocalDateTime.now() + "" + name + "準備開始取錢"); //(同步代碼塊)加鎖 synchronized (this) { if (this.money >= money) { System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元"); this.money -= money; } else { System.out.println(LocalDateTime.now() + "" + name + "餘額不足"); } System.out.println(LocalDateTime.now() + "賬戶的餘額是" + this.money + "元"); } }
同步鎖的註意事項
-
對於當前同時執行的線程來說,同步鎖必須是同一把鎖(同一個對象),否則會出bug。
鎖對象的使用規範
-
建議使用共用資源作為鎖對象,對於實例方法建議使用this作為鎖對象。
-
對於靜態方法建議使用位元組碼(類名.class)對象作為鎖對象。
同步方法
作用:把訪問共用資源的核心代碼給上鎖,以此保證線程安全。
原理:每次只允許一個線程加鎖後進入,執行完畢後自動解鎖,其他線程次才可以來執行。
對線程安全改造
//同步方法加鎖(修飾符後面,放回值類型前面) public synchronized void takeMoney(Integer money) { String name = Thread.currentThread().getName(); System.out.println(LocalDateTime.now() + "" + name + "準備開始取錢"); if (this.money >= money) { System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元"); this.money -= money; } else { System.out.println(LocalDateTime.now() + "" + name + "餘額不足"); } System.out.println(LocalDateTime.now() + "賬戶的餘額是" + this.money + "元"); } }
同步方法底層原理
-
同步方法其實底層也是有隱式鎖對象的,只是鎖的範圍是整個方法代碼。
-
如果方法是實例方法:同步方法預設用this作為的鎖對象。
-
如果方法是靜態方法:同步方法預設用類名.class作為的鎖對象。
1.同步方法是如何保證線程安全的?
-
對出現問題的核心方法使用**synchronized修飾**
-
每次只能一個線程占鎖進入訪問
2.同步方法的同步鎖對象的原理?
-
對於實例方法預設使用**this作為鎖對象。**
-
對於靜態方法預設使用**類名.class對象作為鎖對象。**
Lock鎖
Lock鎖是JDK5開始提供的一個新的鎖定操作,通過它可以創建出鎖對象進行加鎖和解鎖,更靈活、更方便、更強大。
Lock是介面,不能直接實例化,可以採用它的實現類ReentrantLock來構建Lock鎖對象。
對線程安全改造
private static final Lock LOCK = new ReentrantLock(); public void takeMoney(Integer money) { LOCK.lock(); try { String name = Thread.currentThread().getName(); System.out.println(LocalDateTime.now() + "" + name + "準備開始取錢"); if (this.money >= money) { System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元"); this.money -= money; } else { System.out.println(LocalDateTime.now() + "" + name + "餘額不足"); } System.out.println(LocalDateTime.now() + "賬戶的餘額是" + this.money + "元"); }finally { LOCK.unlock(); } }
線程池
瞭解線程池
什麼是線程池?
線程池就是一個可以復用線程的技術。
不使用線程池的問題
用戶每發起一個請求,後臺就需要創建一個新線程來處理,下次新任務來了肯定又要創建新線程處理,而創建新線程的開銷是很大的,並且請求過多時,肯定會產生大量的線程,這樣會嚴重影響系統的性能。
線程池的工作原理
創建線程池
如何得到線程池對象?
方式一:使用ExecutorService的實現類ThreadPoolExecutor創建一個線程池對象。
方式二:使用Executors(線程池的工具類)調用方法返回不同特點的線程池對象。
ThreadPoolExecutor**構造器**
-
參數一:corePoolSize : 指定線程池的核心線程數量。
-
參數二:maximumPoolSize:指定線程池的最大線程數量。
-
參數三:keepAliveTime :指定臨時線程的存活時間。
-
參數四:unit:指定臨時線程存活的時間單位(秒、分、時、天)
-
參數五:workQueue:指定線程池的任務隊列。
-
參數六:threadFactory:指定線程池的線程工廠。
-
參數七:handler:指定線程池的任務拒絕策略(線程都在忙,任務隊列也滿了的時候,新任務來了該怎麼處理)
public class PoolDemo1 { public static void main(String[] args) { //基於ThreadPoolExecutor的構造方法創建線程池對象 //核心線程:【線程任務:Cpu密集型(運算):當前機器Cpu的核心數+1 Runtime.getRuntime().availableProcessors()+1】 //核心線程:【線程任務:IO密集型(讀寫):當前機器Cpu的核心數*2 Runtime.getRuntime().availableProcessors()*2】 //ThreadFactory:線程工廠 【Exectors.defaultThreadFactory】 獲取預設的線程工廠 ThreadPoolExecutor pool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors() + 1, 15, 40L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); //可以基於線程池規範介面的execute方法提交線程任務交給線程池執行 for (int i = 1; i <= 25; i++) { pool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "執行了線程任務!"); } }); } //AbortPolicy:預設丟棄新任務並且拋出異常 //DiscardPolicy:預設丟棄新任務並且不拋出異常 //DiscardOldestPolicy:預設將等待時間最長的任務丟棄,並且讓新任務添加到隊列中 //CallerRunsPolicy:使用主線程執行新任務繞過當前線程池 //線程池一旦提交任務就持久運行【想要關閉調用shutdown/shutdownNow】 pool.shutdown(); } }
線程池的註意事項
1、臨時線程什麼時候創建?
新任務提交時發現核心線程都在忙,任務隊列也滿了,並且還可以創建臨時線程,此時才會創建臨時線程。
2、什麼時候會開始拒絕新任務?
核心線程和臨時線程都在忙,任務隊列也滿了,新的任務過來的時候才會開始拒絕任務。
線程池如何處理Runnable任務?
-
使用ExecutorService的方法:
-
void execute(Runnable target)
線程池如何處理Callable任務,並得到任務執行完後返回的結果?
-
使用ExecutorService的方法:
-
Future<T> submit(Callable<T> command)
併發,並行
併發的含義
進程中的線程是由CPU負責調度執行的,但CPU能同時處理線程的數量有限,為了保證全部線程都能往前執行,CPU會輪詢為系統的每個線程服務,由於CPU切換的速度很快,給我們的感覺這些線程在同時執行,這就是併發。
並行的理解
在同一個時刻上,同時有多個線程在被CPU調度執行。
簡單說說多線程是怎麼執行的?
-
併發:CPU分時輪詢的執行線程。
-
並行:同一個時刻多個線程同時在執行。
線程的生命周期
Java線程的狀態
-
Java總共定義了6種狀態
-
6種狀態都定義在Thread類的內部枚舉類中。