1.認識線程
我們在之前認識了什么是多進程,今天我們來了解線程。
一個線程就是一個 "執行流". 每個線程之間都可以按照順訊執行自己的代碼. 多個線程之間 "同時" 執行 著多份代碼.
引入進程這個概念,主要是為了解決并發編程這樣的問題。因為cpu進入了多核心的時代,要想進一步提高程序的執行速度,就需要充分的利用CPU的多核資源。
其實多進程編程,已經可以解決并發編程的問題了,它已經可以利用起來cpu多核資源了,但是問題是:
進程太重了(消耗資源多、速度慢)創建一個進程,開銷比較大。
銷毀一個進程,開銷也比較大。? ? ? ? ? ?
調度一個進程,開銷還比較大。
說進程重,主要就是重在資源分配/回收上。
線程應運而生,線程也叫做"輕量級進程",
解決并發編程問題的前提下,讓創建,銷毀,調度的速度更快一些
線程為啥更"輕",把申請資源/釋放資源的操作給省下了。
1.1 進程和線程的區別
進程是包含線程的。每個進程至少有一個線程存在,即主線程。
進程和進程之間不共享內存空間。同一個進程的線程之間共享同一個內存空間。
進程是系統分配資源的最小單位,線程是系統調度的最小單位。
光靠文字可能有點抽象,我們舉個例子:
多進程:
?多線程:
在多進程中,啟用了兩套院子,那么啟用的成本是比較大的,耗費的時間也是比較多的,但是在第二套中,院子和運輸材料的通道都是公用的,那么就節省了成本。
在啟動一個新的生產線時,就不需要重新啟動一個院子,而是在原來的院子里啟用,節省了許多的成本。
線程和進程的關系,是進程包含線程,
一個進程可以包含一個線程,也可以包含多個線程,但是不能沒有。
對比下來,主要的優勢在于:
只有第一個線程啟動的時候,開銷是比較大的,但是后續線程就省事了.,不論是啟動還是關閉,耗費的資源都比啟動/關閉一個進程要小。同一個進程里的多個線程之間,共用了進程的同一份資源(主要指的是內存和文件描述符表)。這樣這一部分資源就不需要重新啟動或關閉。
操作系統,實際調度的時候,是以線程為單位進行調度的。
之前介紹的,,PCB里的狀態,上下文,優先級,記賬信息,都是每個線程有自己的。各自記錄各自的但是同一個進程里的PCB之間, ,pid是一樣的,內存指針和文件描述符表也是一樣的。
那么既然線程這么好,可不可以無限制的在一個進程中增加線程呢?
并不可以,線程如果太多,核心數量有限,那么不少的開銷就會浪費在線程調度上了,但是在多進程中就不會出現這樣的狀況。
線程模型,天然就是資源共享的.多線程爭搶同一個資源(同一個變量)非常容易觸發的.
進程模型,天然是資源隔離的.不容易觸發.進行進程間通信的時候,多個進程訪問同一個資源,可能會出問題.
?
1.2 多線程編程
本身關于線程的操作,操作系統提供的API,我們只需要學習Java提供的API就好了。
Java操作多線程,最核心的類 :Thread?
先在src下創建一個包,接著再創建一個類?
創建好主函數后,我們新建一個Thread的對象
Thread t = new Thread();
但是我們還需要一個類,新建一個Mythread類,并且重寫run方法
class MyThread extends Thread{@Overridepublic void run() {System.out.println("hello world");}
}
然后在main中,開始啟動一個特殊的方法:
t.start;
完整的代碼:
class MyThread extends Thread{@Overridepublic void run() {System.out.println("hello world");}
}
public class ThreadDemo1 {public static void main(String[] args) {Thread t = new MyThread();t.start();System.out.println("hello main");}
}
這樣一個代碼,就新啟動了一個線程,使得打印hello world和打印hello main是以完全不同的方式來完成的。
start這里的工作,就是創建了一個新的線程,新的線程負責執行重寫過后的t.run。
具體的執行方法,就是start這個方法會調用操作系統的API,通過操作系統內核創建新線程的PCB,并且把要執行的指令交給PCB,當PCB被調度到CPU上執行的時候,也就執行到了線程run方法中的代碼了。
通過具體的結果,,兩個線程是同時進行的,并且可以看做是一次運行時無序,可能先打印world,也可能先打印main。
但是運行的時候不一定誰先誰后,
操作系統調度線程的時候,"搶占式執行",具體哪個線程先上,哪個線程后上,不確定,取決于操作系統調度器具體實現策略.
雖然有優先級,但是在應用程序層面上無法修改.
從應用程序(代碼)的角度,看到的效果,就好像是線程之間的調度順序是"隨機"的一樣.
內核里本身并非是隨機.但是干預因素太多,并且應用程序這一層也無法感知到細節,就只能認為是隨機的了。
為啥會有線程安全問題?罪魁禍首,萬惡之源,就是這里的搶占式執行,隨機調度。
start和run的區別
start是真正創建了一個線程(從系統這里創建的),線程是獨立的執行流。
run 只是描述了線程要干的活是啥,如果直接再main中調用run,此時沒有創建新線程,全是main線程一個人干活。相當于還是單線程。
可以使用jdk自帶的工具jconsole查看當前的java進程中的所有線程.?
????????
這里面就可以看到進程。同時進程中還有很多個線程。除了我們使用的,其他的都是JVM自帶的
?
?1.3 多線程的五種創建方法
1.繼承Thread,重寫run方法
class MyThread extends Thread{@Overridepublic void run() {System.out.println("hello world");}
}
public class ThreadDemo1 {public static void main(String[] args) {Thread t = new MyThread();t.start();System.out.println("hello main");}
}
也就是上面詳細介紹的方法。
2.實現 Runnable 接口
class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("hello thread");}
}
public class ThreadDemo2 {public static void main(String[] args) {Runnable runnable = new MyRunnable();Thread t = new Thread(runnable);t.start();}
}
Runnable 作用,是描述一個“要執行的任務”,然后把這個任務交給Thread來執行。
好處就是這樣寫可以解耦合,讓線程和線程之間干的活要分開。
3.使用匿名內部類,繼承 Thread
public class ThreadDemo3 {public static void main(String[] args) {Thread t = new Thread(){@Overridepublic void run() {System.out.println("hello");}};t.start();}
}
這里面創建了一個Thread的子類,并且創建了子類的實例,讓 t 引用指向該實例。
4.使用匿名內部類,實現 Runable
public class ThreadDemo4 {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("hello demo4");}});t.start();}
}
這個寫法和2本質相同,只不過是把Runnable任務交給匿名內部類的語法。
此處是創建了一個類,實現Runnable,同時創建了類的實例,并且傳給Thread的構造方法。
5.使用 Lambda 表達式(推薦)
public class ThreadDemo5 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("hello demo5");});t.start();}
}
使用lambda表達式來描述,直接把lambda傳給Thread構造方法。
?