進程和線程
在學習線程之前,首先要理解什么是進程。打開你的任務管理器,導航欄第一個清清楚楚的寫着進程,點進去會發現是許許多多的你在運行的程序,這就是一個進程。
like this:
現代操作系統都可以同時執行多個程序,這就是多任務。線程時建立在進程的基礎上的,比如QQ音樂這個進程可以同時在執行播放、下載、傳輸等動作。這就叫多線程,每個線程在執行不同的功能。
在單核CPU系統中,也可以同時運行多個程序,程序運行是搶占式的,QQ
運行0.001S
,chrome
運行0.01s
,這個時間人是感知不出來的,我們就會覺得在同時執行。所以為了提高效率,現在的手機、電腦都是非常多核的。
進程和線程的關系就是:一個進程可以包含一個或多個線程,但至少會有一個線程。
操作系統調度的最小任務單位其實不是進程,而是線程。
進程 VS 線程
進程和線程是包含關系,但是多任務既可以由多進程實現,也可以由線程實現,還可以混合多進程+多線程。
和多線程相比,多進程的缺點是:
- 創建進程比創建線程開銷大很多,尤其是在Windows上
- 進程間通信比線程要慢,因為線程見通信就是讀寫同一個變量,速度很快
多進程的優點:
- 多進程穩定性比多線程高,因為在多進程情況下,一個進程的崩潰不會影響其他進程,任何一個線程崩潰會導致整個進程崩潰。
創建線程
1. Thread
例:
public class MyThread extends Thread { // 線程的主體類
@Override
public void run(){
System.out.println("Thread is starting");
}
}
上面的MyThread
類繼承Thread
,覆寫了run
方法。一個類只要繼承了此類,就表示這個類為線程的主體類。run()
是線程的主方法,多線程要執行的方法都在這寫。
但是run()
方法是不能被直接調用的,這牽涉到系統的資源調度問題,想要啟動多線程,必須用start()
完成。
調用線程
public class ThreadDemo {
public static void main(String[] args) {
new MyThread().start();
// 啟動新線程
}
java語言內置了多線程支持。當Java程序啟動的時候其實是啟動了一個JVM進程。JVM啟動主線程來執行main()
方法,在main()
方法中可以啟動其他線程。
start()
只能由 Thread
類型實例調用,表示啟動一個線程。
執行結果
"C:\Program Files\Java\jdk1.8.0_221\bin\java.exe"
Thread is starting
由此可見,線程創建成功
那么創建一個多線程呢?
創建多線程
// 多線程主體類
public class MyThread extends Thread {
private String title;
public MyThread(){
}
MyThread(String title){
this.title = title;
}
@Override
public void run(){
for (int i = 0; i<10;i++){
System.out.println(this.title + "is starting");
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
new Thread(new MyThread("A"),"線程1").start();
new Thread(new MyThread("C"),"線程2").start();
new Thread(new MyThread("B")).start();
}
執行結果:
這個結果中有幾個關注點:
- 多線程的執行是無序的,不可控的
- 調用的是
start()
方法,但執行的是run()
方法
我們來看一下源碼,分析一下
public synchronized void start() {
if (threadStatus != 0) // 判斷線程狀態
// 每一個線程的類的對象只允許啟動一次,重復啟動就會拋出這個異常,由run()拋出
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
// 調用此方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
// 注釋部分被我刪掉了,太長了
我們發現start()
方法調用的是start0()
,而start0()
並沒有實現,還被native
修飾,那么native
是啥呢?
在Java程序執行的過程中考慮到對於不同層次的開發者需求,支持了本地的操作系統函數調用。這項技術被稱為JNI(Java Native Interface)
,但在Java開發過程中並不推薦這樣使用。利用這項技術,可以利用操作系統提供的的底層函數,操作一些特殊的處理。
不同的系統在進行資源調度的時候由自己的一套算法,要想調用start()
方法啟動線程,就要實現start0()
,這時候JVM
就會根據不同的操作系統來具體實現start0()
,總結就是一切的一切都是跨平台帶來的。
這也規定了,啟動多線程只有一種方案,調用Thread
類中的start()
方法.
3. Thread 構造函數可以接收一個實例對象和線程的名字參數。
Thread.currentThread().getName()
就代表了獲取當前線程的名字。
在返回值中還出現了"Thread-3",這是由於Thread會自動給沒有命名的線程分配一個不會重復的名字。
這種方式啟動多線程固然沒錯,但存在單繼承的隱患。下面就給出另一種模式。
2. Runnable
首先分別來看一下Thread
類的實現
public
class Thread implements Runnable {}
原來Thread
是繼承了Runnable
再看一下Runnable
接口
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
再次驚訝,原來這個run方法也是從這里繼承的。
那就清楚了,來試一下吧。
// 只需要實現 Runnable,重寫run()即可,其他絲毫未變
public class MyThread implements Runnable {
private String title;
public MyThread(){
}
MyThread(String title){
this.title = title;
}
@Override
public void run(){
for (int i = 0; i<10;i++){
System.out.println(this.title + "is starting");
System.out.println(Thread.currentThread().getName());
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
new Thread(new MyThread("A線程"),"線程1").start();
new Thread(new MyThread("C線程"),"線程2").start();
new Thread(new MyThread("B線程")).start();
// lambda 語法實現
// new Thread(() -> {
// System.out.println("啟動新的線程");
// }).start();
}
}
結果:
完全一致。
在以后的多線程設計實現,優先使用Runnable
接口實現。
還沒完,我們依靠Runnable
接口實現的時候,會發現有一個缺陷,就是沒有返回值,那有沒有帶返回值的實現方式呢?有!繼續看
3. Callable
在Java1.5
之后為了解決run()
方法沒有返回值的問題,引入了新的線程實現java.util.concurrent.Callable
接口.
我們看一下Oracle的api文檔:
可以看到Callable定義的時候利用了一個泛型,代表了返回數據的類型,避免了向下轉型帶來的安全隱患
了解向下轉型可以看我的另一篇文章:https://www.cnblogs.com/gyyyblog/p/11806601.html
但是問題又來了,我們上面已經說過了,要想啟動多線程,必須使用Thread
類提供的
start()
方法調用Runnable
接口的 run()
方法,可是現在 Callable
中並沒有run()
方法,那怎么辦呢?
再來找到一個FutureTask
類:
public class FutureTask<V>
extends Object
implements RunnableFuture<V>
構造方法:
它的構造方法可以接收一個Callable
類型參數
它又繼承了RunnableFuture<V>
,那就繼續往上找
public interface RunnableFuture<V>
extends Runnable, Future<V>
出現了,它出現了,Runnable
我們知道了,是沒有返回值的,現在看看Future<V>
是個啥
它有一個get()
方法可以得到一個泛型返回值。
OK,現在我們就可以梳理一下找到的這些東西怎么個關系:
具體實現
public class CallableThread implements Callable<String> { // 繼承實現Callable<V>
// 覆寫call()方法
@Override
public String call() throws Exception{
for (int i = 0;i<10;i++){
System.out.println("*********線程執行、i="+ i);
}
return "線程執行完畢";
}
}
// 調用
FutureTask<String> task = new FutureTask<>(new CallableThread());
new Thread(task).start();
System.out.println("【線程返回數據】" + task.get());
結果:
為了得到一個返回值可真不容易,核心思想還是實例化一個Thread
對象,可通過其構造方法接收一個Rannable
類型參數,調用start()
啟動線程.
總結:基本來說就這三種創建多線程模式,根據場景使用。
**純屬個人理解,希望指正錯誤,共同交流。