1.進程與線程的概念
(1)進程
程序有指令與數據組成,指令要運行,數據要讀寫,就必須指令加載到CPU。數據加載到內容,指令運行需要用到磁盤。
當一個程序被運行時,從磁盤加載這個程序的代碼至內存,這時開啟了一個進程。
(2)線程
一個進程之內可以分為一個或多個線程。
一個線程就是一個指令流,將指令流中的一條條指令以一定的順序交給CPU執行。
Java中的 最小調度單位是線程,最小資源分配單位是進程。
2.并發與并行的概念
并發:同一時間應對多件事情的能力。
并行:同一時間動手做多件事情的能力。
3.異步與同步的概念
異步:不需要等待結果返回,就能繼續運行
同步:需要等待結果返回,才能繼續運行
同步在多線程中是讓多個線程步調一致
4.多線程提高效率的結論
(1)單核CPU下,多線程不能實際提高程序運行效率,只是為了能夠在不同任務之間切換,不同
線程輪流使用CPU,不至于一個線程總是占用一個CPU,別的線程沒法干活。
(2)多核CPU下,可以并行運行多個線程,能否提高效率要分情況:
一些任務可以通過設計,將任務拆分,并行執行,可以提高運行效率。
不是所有任務都需要拆分,任務的目的不同。
(3)IO操作不占用CPU,只是我們一般拷貝文件使用的是 阻塞IO,相當于線程雖然不用CPU,但是需要等待IO結束,沒能充分利用線程,才有了 非阻塞IO 和 異步IO?優化
5.創建線程
5.1 Thread的方式
Thread thread = new Thread(){@Overridepublic void run(){System.out.println("Hello JUC");}};thread.start();
5.2 Runnable的方式
Runnable runnable = new Runnable(){@Overridepublic void run(){System.out.println("Hello JUC Runnable");}};Thread thread = new Thread(runnable);thread.start();
5.3 Lambda的方式
5.3.1 Thread的Lambda寫法
Thread thread = new Thread(()->{ System.out.println("Hello JUC Lambda");});thread.start();
5.3.2 Runnable的Lambda寫法
Runnable runnable = ()->{System.out.println("Hello JUC Lambda");};Thread thread = new Thread(runnable);thread.start();
5.4 FutureTaks 配合 Thread
FutureTasks 能夠接收Callable類型的參數,用于·處理有返回結果的情況
@Testvoid testFutureTask() throws ExecutionException, InterruptedException {FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {System.out.println("Running ...");Thread.sleep(3000);return 100;}});Thread thread = new Thread(task);thread.start();System.out.println(task.get());}
6.線程運行
線程運行現象:線程運行交替執行。誰先誰后,不由我們所控制。
查詢線程的方式:
(1)windows:
可以通過任務管理器查看進程和線程數
tasklilst 查看進程
taskkill 殺死進程
(2)linux:
ps -fe 查看所有進程
ps -fT -p <PID> 這將顯示PID進程及其所有線程的詳細信息。
kill 殺死進程
top 按大寫H切換是否顯示線程
top -H -p <PID> 查看某個進程(PID)
(3)Java:
jps 命令查看所有Java進程
jstack <PID> 查看某個Java進程(PID)的所有線程狀態
jconsole 來查看某個Java進程中線程的運行情況
7.線程運行的原理
7.1棧幀
JVM是由堆、棧、方法區所組成的,其中棧內存 存放就是線程,每個線程啟動后,虛擬機就會分配一塊棧內存。
每個棧有多個棧幀,對應著每次方法調用時所占用的內存。
每個線程只有一個活動棧幀,對應著當前正在執行的那個方法。
7.2上下文切換(Thread Context Switch)
可能因為
- 線程的cpu時間用完
- 垃圾回收
- 有更高優先級的線程需要運行
- 線程自己調用了sleep、yield、wait、join、park、synchronized、lock等方法
當Thread Context Switch發送時,需要操作系統保存當前線程的狀態,并恢復另一個線程的狀態,Java中對應的概念就是程序計數器,它的作用記錄下一條jvm指令的執行地址,是線程私有的。
狀態包括程序計數器,虛擬機中每個棧幀的信息,如局部變量,操作數棧,返回地址等
Thread Context Switch頻繁發生會影響性能
8.線程中的常見方法
方法 | 描述 |
start() | 啟動新線程并執行該線程的run() 方法。線程開始執行時,它的run() 方法會被調用。 |
run() | 當線程啟動時,run() 方法會被調用。這個方法應該包含線程的執行代碼。 |
join() | 等待調用join() 方法的線程終止。例如,thread.join() 會使當前線程等待thread 線程終止。 |
sleep(long millis) | 使當前線程暫停執行指定的毫秒數。線程不會失去任何監視器的所有權。 |
interrupt() | 中斷線程。如果線程在調用Object 類的wait() 、wait(long) 、wait(long, int) 、join() 、join(long, int) 或者sleep(long, int) 方法時被阻塞,那么它的中斷狀態將被清除,并且它將接收到InterruptedException 。 |
isAlive() | 測試線程是否處于活動狀態。如果線程已經啟動且尚未終止,則線程處于活動狀態。 |
setName(String name) | 改變線程的名稱,可以通過getName() 方法獲取線程的名稱。 |
getPriority() | 返回線程的優先級。線程的優先級可以設置為MIN_PRIORITY (1)、NORM_PRIORITY (5)或MAX_PRIORITY (10)。 |
setPriority(int newPriority) | 變線程的優先級。線程優先級只是建議給調度器,實際調度可能會忽略它。 |
yield() | 暫停當前正在執行的線程對象,并執行其他線程。 |
getState() | 返回線程的狀態。線程的狀態可以是NEW 、RUNNABLE 、BLOCKED 、WAITING 、TIMED_WAITING 或TERMINATED 。 |
isInterrupted() | 測試線程是否已經中斷。不同于interrupted() 方法,這個方法不會改變線程的中斷狀態。 |
8.1 run 與 start 的區別
(1)run()
?方法:
run()
?方法是線程的執行主體,它包含了線程要執行的任務。- 當線程被啟動時,
run()
?方法會被自動調用。 run()
?方法可以直接調用,就像調用普通方法一樣,它在當前線程中執行,而不是在新線程中執行。- 如果直接調用?
run()
?方法,程序不會創建新的線程,而是在當前線程中順序執行?run()
?方法中的代碼。
(2)start()
?方法:
start()
?方法用于啟動一個新線程,并執行該線程的?run()
?方法。- 當?
start()
?方法被調用時,Java 虛擬機會創建一個新的線程,并執行?run()
?方法中的代碼。 start()
?方法只能被調用一次,多次調用會拋出?IllegalThreadStateException
。- 調用?
start()
?方法后,線程可能會立即開始執行,也可能因為線程調度器的安排而在稍后執行。
8.2 Sleep和yield的區別
(3)Sleep
- 調用sleep讓當前線程從Running進入Timed Waiting(阻塞)狀態
- 其他線程可以通過interrupt方法打斷正在睡眠的線程,這時sleep方法會拋出InterruptedException
- 睡眠解釋后的線程未必會立即執行
- 建議使用Timeunit的sleep代替Thread的sleep,有更好的可讀性
(4)yield
- 調用yield 會使當前線程從Running進入Runnable就緒狀態,然后調度執行其他線程
- 具體的實現依賴操作系統的任務調度器
- 在Java中,
Thread.yield()
也是一個靜態方法,它使當前線程從運行狀態轉到可運行狀態,但不會釋放所占有的任何資源。 - 通常,
yield
的使用并不頻繁,因為它對線程調度提供的信息有限,且線程調度器可能會忽略這個提示。
8.3 線程優先級
- 線程優先級會提示調度器優先調度線程
- 如果CPU比較忙,那么優先級高的線程會獲得更多的時間片;如果CPU比較空閑,優先級幾乎沒有什么作用。
8.4 Sleep實現
在沒有利用CPU實現計算時,不要讓while(true)空轉浪費CPU,這時可以使用yield或sleep使CPU的使用權讓給其他的程序
8.5 join實現
等待thread
線程運行結束或終止。
如果join(long n)帶參數的話,就是等待線程運行結束的最多等待n毫秒
8.6 打斷阻塞
阻塞
打斷sleep的線程,清空打斷狀態
示例:
@Testvoid test() throws ExecutionException, InterruptedException {Thread t1 = new Thread(()->{try{TimeUnit.SECONDS.sleep(12);}catch (Exception e){e.printStackTrace();}},"t1");t1.start();TimeUnit.SECONDS.sleep(2);t1.interrupt();System.out.println(t1.isInterrupted());}
注意:?使用 isInterrupted()來判斷是否打斷成功,對于打斷sleep的結果為false則表示打斷成功,同理wait和join都是false標記為打斷成功。
8.7 打斷正常
打斷正常的則isInterrupted()為true則表示打斷成功
示例:
@Testvoid test() throws InterruptedException {Thread t1 = new Thread(()->{try{while (true){boolean interrupted = Thread.currentThread().isInterrupted();if (interrupted){log.info("打斷...");break;}}}catch (Exception e){e.printStackTrace();}},"t1");t1.start();TimeUnit.SECONDS.sleep(3);log.info("interrupt");t1.interrupt();}
9.兩階段終止
問題:在一個線程T1中如何優雅的終止線程T2呢?
錯誤的思路:
- 使用stop方法終止線程,這種如果此時線程鎖住了公共享資源,將其殺死后就再也沒有機會釋放資源,導致其他線程永遠無法獲取鎖。
- 使用System.exit(int)方法停止線程,目的是僅停止一個線程,但是會讓整個程序都停止
正確的思路:
@Testvoid test() throws InterruptedException {Thread t1 = new Thread(()->{while (true){Thread current = Thread.currentThread();if(current.isInterrupted()){log.info("打斷....");break;}try {Thread.sleep(800);log.info("正在記錄中.....");}catch (InterruptedException e){e.printStackTrace();//重置設置打斷標識,因為sleep過程中進行interrupt是標識false,那么需要正常的interrupt使其標識為truecurrent.interrupt();}}});t1.start();TimeUnit.SECONDS.sleep(3);t1.interrupt();}
10.打斷park線程
打斷線程后不會影響標記
11.不推薦的方法
這些方法過時,容易破壞同步代碼塊,造成線程死鎖
方法名 | 說明 |
stop() | 停止線程運行 |
suspend() | 掛起(暫停)線程運行 |
resume() | 恢復線程運行 |
12.主線程與守護線程
默認情況下,Java進程需要等待線程運行都結束,才會結束。有一種特殊的線程是守護線程,只要其他的線程運行結束了,即使守護線程的代碼還沒有執行結束,也會強制結束。
示例:設置t1線程為守護線程:
t1.setDaemon(true);
運用:
- 垃圾回收器線程就是一種守護線程
- 接收shutdown命令后,不會等待Acceptor和Poller守護線程處理完當前請求
13.線程的五種狀態
五種狀態:初始狀態、可運行狀態、運行狀態、終止狀態、阻塞狀態
- 初始狀態:僅在語言層面創建了線程對象,還未與操作系統線程關聯
- 可運行狀態;也稱(就緒狀態),指線程已經被創建(與操作系統線程未關聯),可以由CPU調度執行
- 運行狀態:指獲取CPU時間片運行中的狀態
- 阻塞狀態:如果調用了阻塞API,這時線程實際不會用到CPU,會導致線程上下文切換,進入阻塞狀態
- 終止狀態:表示線程已經執行完畢,生命周期已經結束,不會轉換為其他狀態
14.共享資源的線程安全(synchronized)
競態條件:發生在至少兩個線程競爭同一資源時,而最終的結果取決于這些線程的執行順序。在大多數情況下,競態條件的出現是由于程序設計上的缺陷,例如沒有適當的同步機制來控制對共享資源的訪問。
為了避免臨界的競態的條件發生,可以使用以下的手段實現:
- 阻塞式的解決方案:Synchronized,Lock
- 非阻塞式的解決方案:原子變量
使用阻塞式的解決方案:Synchronized(對象鎖),采用互斥的方式讓同一時刻至多只有一個線程能持有 對象鎖,其他線程再想獲取這個 對象鎖 就會被阻塞住。這樣可以保證線程可以安全的執行臨界區內的代碼,不用擔心線程的上下文切換
- 互斥是保證臨界區的競態條件發生,同一時刻只有一個線程執行臨界區代碼
- 同步是由于線程執行的先后,順序不同,需要一個線程等待其他線程運行到某個點
?示例代碼:
int counter = 0;Object lock = new Object();@Testvoid test() throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0;i<=50000;i++){synchronized(lock){counter++;}}});Thread t2 = new Thread(()->{for (int i = 0;i<=50000;i++){synchronized(lock){counter--;}}});t1.setName("t1");t2.setName("t2");t1.start();t2.start();t1.join();t2.join();log.info("Counter:{}",counter);}
synchronized用對象鎖保證了臨界區內代碼的原子性,臨界區的代碼對外不可分割,不會被線程切換所打斷
思考:
如果synchronized(obj)放在for循環外面,如何理解? --原子性
如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 會怎么運行?--鎖對象
如果 t1 synchronized(obj)而 t2沒有加會怎么樣?--鎖對象
14.1 通過面向對象的方式實現:
創建實體:
public class Room {private int counter = 0;public void increment(){synchronized (this){counter++;}}public void decrement(){synchronized (this){counter--;}}public int getCounter(){synchronized (this){return counter;}}
}
實現代碼:
Room room = new Room();@Testvoid test() throws InterruptedException {Thread t1 = new Thread(()->{room.increment();});Thread t2 = new Thread(()->{room.decrement();});t1.setName("t1");t2.setName("t2");t1.start();t2.start();t1.join();t2.join();log.info("Counter:{}",room.getCounter());}
? ? ? 14.2 synchronized加在方法上
(1)
public class Test {public synchronized void test(){}}
相當于:
public class Test {public void test(){synchronized(this){}}}
(2)static方法
public class Test {public synchronized static void test(){}}
相當于:
public class Test {public static void test(){synchronized(Test.class){}}}
15.變量線程安全分析
15.1 成員變量和靜態變量是否線程安全?
(1)如果沒有共享,則線程安全
(2)如果共享,根據他們的狀態是否能夠改變,分為兩種情況:
- 如果只有讀操作,則線程安全
- 如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全
15.2 局部變量是否線程安全?
(1)局部變量是線程安全的
(2)但是局部變量引用的對象則未必
- 如果該對象沒有逃離方法的作用訪問,它是線程安全的
- 如果該對象逃離方法的作用范圍,則需要考慮線程安全
16.常用的線程安全類
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.utils.concurrent包下的類
這里的線程安全是多線程調用它們同一個實例的某個方法時,線程是安全的。可以理解為
- 它們的每個方法是原子
- 但是它們的多個方法的組合不是原子的(不是線程安全的)
不可改變線程安全
String、Integer等都是不可變類,因為其內部的狀態不可改變,因此它們的方法都是線程安全的
雖然String有replace、substring等方法可以改變值、但是根據源碼可知都是創新一個新的對象,沒有改變原有的對象的值,所以線程安全
17.買票問題習題
(1)線程安全問題示例
package org.example;import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;// 主類,用于執行售票模擬
public class ExerciseSell {// 執行售票模擬的入口點public static void main(String[] args) throws InterruptedException {// 創建一個 TicketWindow 實例,初始票數為1000TicketWindow ticket = new TicketWindow(1000);// 創建一個線程列表,用于存放即將創建的線程List<Thread> threadList = new ArrayList<>();// 創建一個Integer類型的列表,用于存放每個線程賣出的票數List<Integer> amountList = new Vector<>();// 創建80000個線程,每個線程將模擬賣出一定數量的票for(int i = 0; i < 80000; i++) {Thread thread = new Thread(() -> {// 每個線程賣出的票數是隨機生成的int amount = ticket.sell(randomAmount());try {// 模擬售票操作后的延遲Thread.sleep(10);} catch (InterruptedException e) {// 如果線程在睡眠中被中斷,拋出運行時異常throw new RuntimeException(e);}// 將賣出的票數添加到amountList中amountList.add(amount);});// 將新創建的線程添加到線程列表中threadList.add(thread);// 啟動線程thread.start();}// 等待所有線程完成for (Thread thread : threadList) {thread.join();}// 打印剩余票數System.out.println("余票:" + ticket.getAmount());// 計算所有線程賣出的票數總和并打印System.out.println("賣出的票數:" + amountList.stream().mapToInt(i -> i).sum());}// 生成隨機售票數的方法static Random random = new Random();public static int randomAmount() {// 返回1到5之間的隨機整數,包括1和5return random.nextInt(5) + 1;}
}// 表示售票窗口的類
class TicketWindow {// 私有屬性,表示售票窗口的票數private int amount;// 構造函數,初始化票數為傳入的參數public TicketWindow(int amount) {this.amount = amount;}// 獲取當前票數的方法public int getAmount() {return amount;}// 售票方法,嘗試從窗口賣出指定數量的票public int sell(int amount) {if (this.amount >= amount) {// 如果票數足夠,減少票數并返回賣出的票數this.amount -= amount;return amount;} else {// 如果票數不足,返回0return 0;}}
}
(2)解決線程安全問題方法
這段代碼存在線程安全問題。具體來說,TicketWindow
?類的?amount
?成員變量在被多個線程訪問和修改時,沒有使用任何同步機制,這可能導致多個線程同時修改?amount
,從而引發數據不一致的問題。
所以要給amount 進行共享資源讀寫的時進行加鎖操作(這段代碼,只需要對sell方法加上synchronized即可加鎖)
public synchronized int sell(int amount){if(this.amount >= amount){this.amount -= amount;return amount;}else {return 0;}}
18. 轉賬習題
(1)線程安全問題示例
?
package org.example;import java.util.Random;public class ExerciseTransfer {public static void main(String[] args) throws InterruptedException {Amount a = new Amount(1000);Amount b = new Amount(1000);Thread t1 = new Thread(()->{for(int i = 0;i < 100;i++){a.transfer(b,randomAmount());}});Thread t2 = new Thread(()->{for(int i = 0;i < 100;i++){b.transfer(a,randomAmount());}});t1.start();t2.start();t1.join();t2.join();System.out.println("總金額:"+a.getMoney() + b.getMoney());}static Random random = new Random();public static int randomAmount(){return random.nextInt(100) + 1;}}class Amount{private int money;public int getMoney() {return money;}public void setMoney(int money) {this.money = money;}public Amount(int money) {this.money = money;}public void transfer(Amount amount,int transferMoney){if(this.money >= transferMoney){this.setMoney(this.getMoney() - transferMoney);amount.setMoney(amount.getMoney() + transferMoney);}}
}
(2)解決線程安全問題方法(兩個共享變量)
public void transfer(Amount amount,int transferMoney){synchronized(Amount.class){if(this.money >= transferMoney){this.setMoney(this.getMoney() - transferMoney);amount.setMoney(amount.getMoney() + transferMoney);}}}
?因為如果
public synchronized void transfer(Amount amount,int transferMoney){if(this.money >= transferMoney){this.setMoney(this.getMoney() - transferMoney);amount.setMoney(amount.getMoney() + transferMoney);}}
相當于保護this.money,但是不保護amount的money
public void transfer(Amount amount,int transferMoney){synchronized(this){if(this.money >= transferMoney){this.setMoney(this.getMoney() - transferMoney);amount.setMoney(amount.getMoney() + transferMoney);}}}