詳解JUC并發(fā)編程中的進程與線程學(xué)習(xí)
進程與線程
進程
- 程序由指令和數(shù)據(jù)組成,但這些指令要運行,數(shù)據(jù)要讀寫,就必須將指令加載至 CPU,數(shù)據(jù)加載至內(nèi)存。在指令運行過程中還需要用到磁盤、網(wǎng)絡(luò)等設(shè)備。進程就是用來加載指令、管理內(nèi)存、管理 IO 的
- 當一個程序被運行,從磁盤加載這個程序的代碼至內(nèi)存,這時就開啟了一個進程。
- 進程就可以視為程序的一個實例。大部分程序可以同時運行多個實例進程(例如記事本、畫圖、瀏覽器等),也有的程序只能啟動一個實例進程(例如網(wǎng)易云音樂、360 安全衛(wèi)士等)
線程
線程是主要負責運行指令,進程是主要管加載指令。
一個進程之內(nèi)可以分為一到多個線程。
一個線程就是一個指令流,將指令流中的一條條指令以一定的順序交給 CPU 執(zhí)行
Java 中,線程作為最小調(diào)度單位,進程作為資源分配的最小單位。 在 windows 中進程是不活動的,只是作為線程的容器
同步異步
- 需要等待結(jié)果返回,才能繼續(xù)運行就是同步
- 不需要等待結(jié)果返回,就能繼續(xù)運行就是異步
串行并行執(zhí)行時間
使用多核cpu并行執(zhí)行可以明顯的提高執(zhí)行效率
- 串行執(zhí)行時間 = 各個線程時間累加和 + 匯總時間
- 并行執(zhí)行時間 = 最慢的線程時間 + 匯總時間
注意:單核依然是并發(fā)的思想(即:cpu輪流去執(zhí)行線程,微觀上仍舊是串行),使用單核的多線程可能會比使用單核的單線程慢,這是因為多線程上下文切換反而浪費了時間。
創(chuàng)建和運行線程
1.使用 Thread
public static void main(String[] args) { // 創(chuàng)建線程對象 Thread t = new Thread("線程1") { public void run() { // 要執(zhí)行的任務(wù) log.debug("線程1被啟動了"); } }; // 啟動線程 t.start(); log.debug("測試"); }
2.使用 Runnable 配合 Thread
public static void main(String[] args) { Runnable runnable = new Runnable() { public void run() { // 要執(zhí)行的任務(wù) log.debug("線程1被啟動了"); } }; // 創(chuàng)建線程對象 Thread t = new Thread(runnable); t.setName("線程1"); // 啟動線程 t.start(); log.debug("測試"); }
這里的Runnable是一個接口,接口中只有一個抽象方法,由我們來提供實現(xiàn),實現(xiàn)中包含線程的代碼就可以了。
@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(); }
Thread 與 Runnable 的關(guān)系原理分析
方法1原理分析
方法2是使用runnable對象,當成參數(shù)傳給Thread構(gòu)造方法,其中又調(diào)用了init方法,下面是Thread構(gòu)造方法的源碼
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
繼續(xù)跟蹤查看runnable對象傳到哪里去了,可以看到又傳給了另一個重載的init,如下
private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); }private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); }
再次跟蹤可以看到是把runnable對象傳給了一個thread的一個成員變量
//省略部分代碼 this.target = target;
那么這個成員變量在哪里在使用了呢,經(jīng)過查找可以發(fā)現(xiàn)是在run方法里面,只不過Thread發(fā)現(xiàn)有runnable對象就會先采用runnable的run方法。
@Override public void run() { if (target != null) { target.run(); } }
方法2原理分析
通過創(chuàng)建一個子類去重寫Thread類的run方法,這樣就不會執(zhí)行父類的run方法。
1.用 Runnable 更容易與線程池等高級 API 配合
2.用 Runnable 讓任務(wù)類脫離了 Thread 繼承體系,更靈活
方法3 FutureTask配合Thread創(chuàng)建線程
Future接口中含有g(shù)et方法來返回結(jié)果的
//省略部分代碼 V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
而runnable本身是沒有返回結(jié)果的,runnable不能將結(jié)果傳給其他線程。
public interface Runnable { public abstract void run(); }
要注意到FutureTask也實現(xiàn)了Runnable接口,也可以傳給Thread的有參構(gòu)造里面。
創(chuàng)建線程的代碼
public static void main(String[] args) throws ExecutionException, InterruptedException { // 創(chuàng)建任務(wù)對象 FutureTask<Integer> task3 = new FutureTask<>(new Callable<Integer>() { @Override public Integer call() throws Exception { log.debug("線程1被執(zhí)行了"); return 666; } }); // 參數(shù)1 是任務(wù)對象; 參數(shù)2 是線程名字,推薦 new Thread(task3, "線程1").start(); // 主線程阻塞,同步等待 task 執(zhí)行完畢的結(jié)果 Integer result = task3.get(); log.debug("結(jié)果是:{}", result); }
查看進程
- 任務(wù)管理器可以查看進程和線程數(shù),也可以用來殺死進程,也可以在控制臺使用
tasklist
查看進程taskkill
殺死進程 - jconsole 遠程監(jiān)控配置來查看
線程運行原理
JVM 中由堆、棧、方法區(qū)所組成,其中棧就是給線程使用的。
方法調(diào)用時,就會對該方法產(chǎn)生一個棧幀,方法的局部變量都會在棧幀中存儲。棧是后進先出,當method2執(zhí)行完就會回收,在執(zhí)行完同時會記錄返回地址,然后在method1中繼續(xù)執(zhí)行。
線程之間的棧幀是相互獨立的,之間互不干擾。
線程上下文切換
當上下文切換時,要保存當前的狀態(tài),因為可能是時間片用完了,此時線程還沒有結(jié)束。Java中對應(yīng)的就是程序計數(shù)器
start與run方法
啟動一個線程必須要用start方法,如果直接調(diào)用類里面的run方法實際走的是main主線程。
線程start前getState()
得到的是NEW
線程start后getState()
得到的是RUNNABLE
public static void main(String[] args) { Thread t1 = new Thread("t1") { @Override public void run() { log.debug("t1被啟動"); } }; System.out.println(t1.getState()); t1.start(); System.out.println(t1.getState()); }
sleep方法
在sleep期間調(diào)用getState()
方法可以得到TIMED_WAITING
public static void main(String[] args) { Thread t1 = new Thread("線程1") { @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.start(); log.debug("線程1 state: {}", t1.getState()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("線程1 state: {}", t1.getState()); }
sleep打斷
sleep可以使用interrupt
方法打斷,打斷后會觸發(fā)InterruptedException
異常
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread("t1") { @Override public void run() { log.debug("進入睡眠"); try { Thread.sleep(2000); } catch (InterruptedException e) { log.debug("被喚醒"); e.printStackTrace(); } } }; t1.start(); Thread.sleep(1000); log.debug("打斷"); t1.interrupt(); }
sleep防止cpu使用100%
在沒有利用cpu來計算時,不要讓while(true)空轉(zhuǎn)浪費cpu,這時可以使用yield或 sleep 來讓出cpu的使用權(quán)給其他程序
yield方法會把cpu的使用權(quán)讓出去,然后調(diào)度執(zhí)行其它線程。線程的調(diào)度最終還是依賴的操作系統(tǒng)的調(diào)度器。
join方法
該方法會等待線程的結(jié)束
static int r = 11; public static void main(String[] args) throws InterruptedException { test1(); } private static void test1() throws InterruptedException { log.debug("主線程開始"); Thread t1 = new Thread(() -> { sleep(1); r = 888; },"線程1"); t1.start(); // t1.join(); log.debug(String.valueOf(r)); log.debug("主線程線程結(jié)束"); }
join沒有使用時,返回的是11,若是使用join返回的是888,是主線程在同步等待線程1。
當然還有其他的方法等待
1.sleep方法等待線程1結(jié)束
2.利用FutureTask的get方法
join(long n)方法可以傳入?yún)?shù),等待線程運行結(jié)束,最多等待 n 毫秒,假如執(zhí)行時間大于等待的時間,就會不再等待。那么該線程會直接結(jié)束嗎?答案是不會。
如下代碼
public class Test10 { static int r = 11; public static void main(String[] args) throws InterruptedException { test1(); } private static void test1() throws InterruptedException { log.debug("主線程開始"); Thread t1 = new Thread(() -> { sleep(2); r = 888; log.debug("線程1結(jié)束"); },"線程1"); t1.start(); t1.join(1000); log.debug(String.valueOf(r)); log.debug("主線程線程結(jié)束"); } }
輸出結(jié)果,可以看到這里只是主線程不再等待。
16:28:53.360 c.Test10 [main] - 主線程開始
16:28:54.411 c.Test10 [main] - 11
16:28:54.411 c.Test10 [main] - 主線程線程結(jié)束
16:28:55.404 c.Test10 [線程1] - 線程1結(jié)束
interrupt 方法
interrupt可以用來打斷處于阻塞狀態(tài)的線程。在打斷后,會有一個打斷標記(布爾值)會提示是否被打斷過,被打斷過標記為true
否則為false
.
但是sleep、wait和join可以來清空打斷標記。
代碼如下
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { log.debug("sleep..."); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } },"t1"); t1.start(); Thread.sleep(1000); log.debug("interrupt"); t1.interrupt(); log.debug("打斷標記:{}", t1.isInterrupted()); }
線程被打斷后并不會結(jié)束運行,有人就會問了,那我們?nèi)绾卧诖驍嗑€程后關(guān)閉線程呢?答案就是利用打斷標記去實現(xiàn)。
可以在線程的死循環(huán)之中加入一個判斷去實現(xiàn)。
boolean interrupted = Thread.currentThread().isInterrupted(); if(interrupted) { log.debug("退出循環(huán)"); break; }
守護進程
Java 進程通常需要所有線程都運行結(jié)束,才會結(jié)束。
但是存在一種守護進程,只要其他非守護進程結(jié)束,守護進程就會結(jié)束。垃圾回收器就使用的守護進程。
線程的狀態(tài)
操作系統(tǒng)層面(早期進程的狀態(tài))
- 初始狀態(tài) 在語言層面創(chuàng)建了線程對象,還未與操作系統(tǒng)線程關(guān)聯(lián)
- 可運行狀態(tài)(就緒狀態(tài))指該線程已經(jīng)被創(chuàng)建(與操作系統(tǒng)線程關(guān)聯(lián)),可以由 CPU 調(diào)度執(zhí)行任務(wù)。
- 運行狀態(tài) 獲取了 CPU 時間片運行中的狀態(tài)
- 調(diào)用阻塞api使運行狀態(tài)轉(zhuǎn)為阻塞狀態(tài)
- 終止狀態(tài) 表示線程已經(jīng)執(zhí)行完畢
Java API 層面
1、新建狀態(tài)(New)
Thread t1 = new Thread("t1") { @Override public void run() { log.debug("running..."); } }; log.debug("t1 state {}", t1.getState());
2、就緒狀態(tài)(Runnable)與運行狀態(tài)(Running)
Thread t2 = new Thread("t2") { @Override public void run() { while(true) { // runnable } } }; t2.start(); log.debug("t2 state {}", t2.getState());
3、阻塞狀態(tài)(Blocked)
用一個線程拿到鎖,使得當前線程沒拿到鎖會出現(xiàn)阻塞狀態(tài)。
Thread t6 = new Thread("t6") { @Override public void run() { synchronized (TestState.class) { // blocked try { Thread.sleep(90000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t6.start();
4、等待狀態(tài)(Waiting)
等待一個未執(zhí)行完成的線程
t2.join(); //等待狀態(tài)
5、超時等待(Time_Waiting)
可以在指定的時間自行返回的。
6、終止狀態(tài)(TERMINATED)
線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。 終止的線程不可再次復(fù)生。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
spring boot 注入 property的三種方式(推薦)
這篇文章主要介紹了spring boot 注入 property的三種方式,需要的朋友可以參考下2017-07-07基于spring+quartz的分布式定時任務(wù)框架實現(xiàn)
在Spring中的定時任務(wù)功能,最好的辦法當然是使用Quartz來實現(xiàn)。這篇文章主要介紹了基于spring+quartz的分布式定時任務(wù)框架實現(xiàn),有興趣的可以了解一下。2017-01-01Java鏈表的天然遞歸結(jié)構(gòu)性質(zhì)圖文與實例分析
這篇文章主要介紹了Java鏈表的天然遞歸結(jié)構(gòu)性質(zhì),結(jié)合圖文與實例形式分析了java鏈表中遞歸操作的原理、實現(xiàn)技巧與相關(guān)注意事項,需要的朋友可以參考下2020-03-03