JAVA併發編程-1-線程基礎
一 、基礎概念
1,cpu核心數與線程數
核心數 : 線程數 = 1 : 1
什麼意思呢,就是說如果是一個8核的cpu,那麼該cpu會至少支持8個線程同時運行
intel引入了超線程技術後:
核心數 : 線程數 = 1 : 2
而我們在編碼過程中可以感覺到同時運行的線程遠遠不止這些。
這是由於cpu的時間片輪轉機制又稱RR調度,簡單點講,操作系統會把已就緒的線程排成一個隊列,給每個進程一個時間分片,該線程在cpu中執行完這個時間分片後,不論是否執行完都會讓出cpu資源給另外的線程,這樣在某一時間段內就好像有很多線程在同時運行。
值得注意的一點是,操作系統和cpu在進行時間分片的任務切換時也是需要時間的,而且往往佔用的時間比例又很大,(舉個例子:時間分片有20ms,切換就要5ms),所以我們在多線程開發時關注上下文切換對於多線程執行時間和性能的影響。
2,進程和線程
進程:程序運行進行資源分配的最小單位,進程中有多個線程,會共享這個線程的資源
線程:cpu調度的最小單位,必須依賴進程而存在
舉個例子:啓動的一個jar包程序就是一個進程,而我們可以通過啓動參數配置它的內存大小,-xmx,-xms等。而每一個請求都是在線程上去進行的,cpu通過執行線程任務完成每個請求任務。
3,並行和併發
並行:同一時刻,可以同時處理事情的能力
併發:與單位時間有關,在單位時間內可以處理問題的能力
舉個例子,假設不考慮超線程技術,一個4核cpu在任何一個時刻處理的是4個線程,並行數爲4,而由於時間片輪轉機制,它在1秒內可以支持處理100個線程,它在1秒內的併發數爲100
4,高併發編程的意義與問題
好處 :
(1)充分利用cpu的資源。如果是單線程,只佔用一個核,其它的空閒
(2)加快響應時間。合理的設計多線程程序,使請求處理加快。
(3)程序模塊化異步化
注意事項:
(1)線程共享資源,會存在衝突
(2)會存在死鎖
(3)啓動線程太多,濫用線程,壓垮服務器
二、實現線程的三種方式
1,繼承 Thread 類
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("我是線程MyThread");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2,實現 Runnable 接口
爲什麼還要有繼承接口的方式呢?因爲java單繼承
public class MyRunThread implements Runnable {
@Override
public void run() {
System.out.println("我是線程MyRunThread");
}
public static void main(String[] args) {
MyRunThread myRunThread = new MyRunThread();
new Thread(myRunThread).start();
}
}
而有了java8 lamda表達式之後,看到Runnable意識到他是個典型的函數式接口,可以更加優雅的實現
public static void main(String[] args) {
new Thread(() -> System.out.println("我是lamda MyRunThread")).start();
}
3,實現 Callable 接口
Callable 接口支持在主線程中拿到子線程的執行結果:
public class MyCallThread implements Callable<String> {
@Override
public String call() throws Exception {
return "我是 MyCallThread 的執行結果 callReslut";
}
public static void main(String[] args) throws Exception {
MyCallThread myCallThread = new MyCallThread();
FutureTask<String> futureTask = new FutureTask<>(myCallThread);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
值得關注的一點是:這裏的futureTask.get()方法不需要顯示的判斷線程是否完成,而是會自己阻塞直到拿到線程的執行結果。有興趣的同學可以去研究下源碼。
三、線程的狀態
線程只有5種狀態,整個生命週期只會在這5鍾狀態下切換。
public enum State {
/**
* 初始狀態,線程被構建new,但是還沒有調用start方法
*/
NEW,
/**
* 運行狀態,java線程將操作系統中的就緒和運行兩種狀態籠統的稱爲"運行中"
*/
RUNNABLE,
/**
* 阻塞狀態,表示線程阻塞於鎖🔒
*/
BLOCKED,
/**
* 等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定動作(通知或中斷)
* 比如通過Object.wait()進入WAITING,就需要Object.notify()或者Object.notifyAll()進行通知
* 調用Thread.join()進入進入WAITING,就需要等待規定的線程執行完成
*/
WAITING,
/**
* 超時等待狀態,表示線程在規定時間內等待
* Thread.sleep(long),Object.wait(long)等方法
*/
TIMED_WAITING,
/**
* 終止狀態,表示當前線程執行完畢
*/
TERMINATED;
}
值得強調的兩點:
1,java線程的運行狀態不同於操作系統的運行狀態,而是對就緒和運行兩種狀態的統稱
2,TIMED_WAITING有的人不理解爲何還有這個狀態。其實可以想一下,有超時時間的等待和無超時時間的等待執行的指令是不一樣的,因爲還要判斷超時,所以需要兩種狀態來描述
四、線程方法
首先我們要記住一個概念,java線程是協作式的,而不是搶佔式的
1, 線程終止的方法interrupt()
線程之前提供了stop(),resume(),suspend()方法來終止線程,但已不建議使用,stop()會導致線程不會正確的釋放資源,suspend()會導致死鎖。
我們要通過interrupt(),isInterrupted(),static interrupted()來自己實現中斷線程
interrupt() :調用一個線程的interrupt() 方法中斷一個線程,並不是強行關閉這個線程,只是跟這個線程打個招呼,將線程的中斷標誌位置爲true,線程是否中斷,由線程本身決定。
isInterrupted(): 判定當前線程是否處於中斷狀態。
static interrupted() :判定當前線程是否處於中斷狀態,同時中斷標誌位改爲false。
public class MyRunThread implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("線程MyRunThread is run");
}
System.out.println("線程MyRunThread interrput flag is " + Thread.currentThread().isInterrupted());
}
public static void main(String[] args) throws Exception {
MyRunThread myRunThread = new MyRunThread();
Thread thread = new Thread(myRunThread);
thread.start();
Thread.sleep(3);
thread.interrupt();
}
}
運行結果:
方法裏如果拋出InterruptedException,線程的中斷標誌位會被複位成false,如果確實是需要中斷線程,要求我們自己在catch語句塊裏再次調用interrupt()。
總結一下,interrupt方法實際上就提供了一個可靠的中斷標誌位,線程在自己的運行過程中去判斷這個標記位,決定自己的中斷與否。實際上我們自己定義一個合適的變量也可以達到目的。
2,等待和通知wait(),notify(),notifyAll()
典型的等待通知機制,也可以理解爲生產者消費者模式
等待和通知的標準範式
等待方:
1、獲取對象的鎖;
2、循環裏判斷條件是否滿足,不滿足調用wait方法,
3、條件滿足執行業務邏輯
通知方來說
1、獲取對象的鎖;
2、改變條件
3、通知所有等待在對象的線程
public class Express {
int i = 0;
public synchronized void check() {
if (i <= 0) {
System.out.println("不滿足條件");
try {
wait();
} catch (InterruptedException e) {
}
}
System.out.println("被喚醒---");
}
public synchronized void change() {
i = 1;
notifyAll();
}
public synchronized void changeSingle() {
i = 1;
notify();
}
}
public class Test {
private static Express express = new Express();
private static class Checki extends Thread {
@Override
public void run() {
express.check();
}
}
public static void main(String[] args) throws InterruptedException{
for (int i = 0; i < 3; i++) {
new Checki().start();
}
Thread.sleep(1000);
// express.change();
// express.changeSingle();
}
}
另外,這3個方法是定義在Object類中而不是Thread類中。
一個很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需要等待某些鎖,那麼調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因爲鎖屬於對象。
3,利用join()保證線程的執行順序
線程A,執行了線程B的join方法,線程A必須要等待B執行完成了以後,線程A才能繼續自己的工作
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(() -> System.out.println("我是第1個線程"));
Thread thread2 = new Thread(() -> System.out.println("我是第2個線程"));
Thread thread3 = new Thread(() -> System.out.println("我是第3個線程"));
thread1.start();
//主線程調用了 thread1 的join,則一定會等待thead1執行完了纔會繼續執行
thread1.join();
thread2.start();
thread2.join();
thread3.start();
}
join底層也是依靠wait()和notify()來實現的,感興趣的同學可以自己看下
4,調用yield() ,sleep(),wait(),notify()等方法對鎖的影響
yield()方法:當前線程在當前時間分片的任務執行完成,讓出cpu執行權。
該方法不會釋放持有的鎖。
sleep(): 不會釋放鎖
調動方法之前,必須要持有鎖。調用了wait()方法以後,鎖就會被釋放,當wait方法返回的時候,線程會重新持有鎖
調動方法之前,必須要持有鎖,調用notify()方法本身不會釋放鎖的,要等待代碼塊跑完。
五、線程的優先級
thead.setPriority()方法
優先級的範圍1~100,缺省爲5,但線程的優先級不可靠,不建議作爲線程開發時候的手段。
原因是java線程是映射到系統的原生線程來實現的,所以線程的調度最終決定於操作系統。雖然現在很多操作系統提供了線程優先級的概念,但是不見得會與java線程的優先級一一對應,如果優先級比java線程多還好說,要是少的話,就不得不出現幾個優先級相同的情況了。
另外還有一些情況讓我們不能太依賴優先級:優先級可能會被系統自行改變。在widows系統存在着一個“優先級推進器”,它的大致作用就是當前系統發現一個線程執行的特別“勤奮努力”的話,可能就會越過線程優先級給它分配時間。