各位看官早安午安晚安呀
如果您覺得這篇文章對您有幫助的話
歡迎您一鍵三連,小編盡全力做到更好
歡迎您分享給更多人哦
今天我們來學習多線程編程-"掌握線程創建、管理與安全":
上一節課程我們鋪墊了一系列的東西,引出來了我們的多線程,接下來就讓小編帶領大家一起進入多線程的世界吧!!!
目錄
上一節課程我們鋪墊了一系列的東西,引出來了我們的多線程,接下來就讓小編帶領大家一起進入多線程的世界吧!!!
1.創建線程的方法:
1.1通過繼承Thread類,然后重寫run()方法
1.2.實現Runnnable接口,重寫run方法
1.3.二者區別
2.(通過jconsole觀察進程里面的多線程情況)
2.1.首先我們要知道每一個線程都是獨立的執行流
2.1.jconsole
3.Thread的一些其他方法
4.中斷一個線程(interrupt)
4.1.手動設置標志位
4.2.Thread內置的標志位
5.線程等待(join方法)
6.線程狀態
7.線程安全問題(最重要)
8.synchronized(原子性)
1.創建線程的方法:
線程實話說是操作系統的概念,程序員想要操作線程肯定就需要操作系統提供給我們一些API,但是不同的操作系統提供的API又不同 (實話說這就讓小編想起了這個)
=>? java就針對上述的系統API進行了封裝(跨平臺嘛,(^-^)V 我們java太厲害啦)
=> 我們程序員只需要了解這一套API就夠啦~~
?java提供給我們的API就是Thread類,我們創建Thread對象就可以操作系統內部的線程啦
老規矩:學習一個類,先看他的構造方法
我們大概先學這幾種,還有一種線程分組的,小編現在也不了解,后續再給大家介紹吧~~
Runnable表示一個可以運行的任務而已,這個任務是交給線程執行還是其他用圖我們不關心
1.1通過繼承Thread類,然后重寫run()方法
class MyThread extends Thread{@Overridepublic void run() {System.out.println("我創建的一個新線程"); // 這個run方法里面描述了我這個線程要干啥}
}
這是我們自己創建的線程,但是一個java程序跑起來,還有一個跟隨進程一起創建的線程,這個線程叫做主線程,這個主線程的入口方法是main方法。
這個run方法呢是我們創建的這個線程的入口方法!!!
一個程序跑起來,從哪個方法開始執行,哪個方法就是他的入口方法。
run方法是入口方法沒有錯,但是這個線程我們想要跑起來,肯定要啟動呀
class MyThread extends Thread{@Overridepublic void run() {System.out.println("我創建的一個新線程");}
}
public class Test {public static void main(String[] args) throws InterruptedException {Thread t = new MyThread();t.start(); // 啟動線程,start方法才是真正的調用系統的API ,// 創建一個線程然后這個線程通過run方法跑起來System.out.println("這就是主線程");}
1.2.實現Runnnable接口,重寫run方法
class MyRunnable implements Runnable {@Overridepublic void run() {while (true) {System.out.println("我實現Runnable,重寫run方法實現的任務");}}
}public class Test {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start();while (true) {System.out.println("主線程");}}}
當然我們通過匿名內部類和lambda表達式實現也完全沒問題(上面我們繼承Thread類重寫run方法當然也可以使用匿名內部類的方式,但是lambda表達式不可以(函數式接口定義(就是lambda表達式):一個接口有且只有一個抽象方法))
Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("通過匿名內部類實現");}});Thread t2 = new Thread(() -> {System.out.println("通過lambda表達式實現");});
1.3.二者區別
首先我們要明確創建線程的兩個關鍵操作
1.首先要明確要執行的任務(就是想要通過這個線程干啥)
2.調用系統API創建出線程。
好,現在我們就可以明確知道他倆的區別了,耦合性不同
1.第一種方法(繼承Thread的)把任務嵌套在了線程里面(后面想要修改任務,就要修改線程的源代碼(大工程))
2.第二種方法:我要讓線程執行其他任務,直接構造方法變成其他任務就好了。并且我這個任務又不是只給一個線程使用,其他線程想用的話直接傳過去就行了。
總之:把任務分離出來,耦合性更低了,效率更高!!!
2.(通過jconsole觀察進程里面的多線程情況)
2.1.首先我們要知道每一個線程都是獨立的執行流
就拿這個代碼來說(但是這個代碼打印的太快了不好觀察,我們可以讓他休眠一下,慢一點打印)
class MyThread extends Thread{@Overridepublic void run() {System.out.println("我創建的一個新線程");}
}public class Test {public static void main(String[] args){Thread t = new MyThread();t.start(); while (true) {System.out.println("主線程");}}
休眠的方法:sleep方法是Thread類的靜態方法(這個異常的處理很有講究的,大家盡量不要犯這樣的錯誤呀)
class MyThread extends Thread{@Overridepublic void run() {while(true){try {System.out.println("我創建的一個新線程");Thread.sleep(1000);// sleep 這里就是一個類方法,我們直接調用就好了// 這里不能拋出異常,如果子類方法拋出額外的異常,調用者(可能只了解父類方法)可能不知道如何正確處理這些新的異常。//這里 父類的 run方法并沒有拋出異常} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}public class Test {public static void main(String[] args) throws InterruptedException {Thread t = new MyThread();t.start();while (true) {Thread.sleep(1000);System.out.println("主線程");}}
可以看到兩個線程的正在交替打印日志,并且不是一種規律進行打印。
你倆都是休眠1000ms,休眠后并且你你倆誰先執行都不一定(隨機的),這個取決于操作系統對于調度器的的具體實現。
通過并發執行,更加充分的的利用CPU資源
2.1.jconsole
jconsole(觀察多線程屬性的一種方式,還有IDEA的一種一個)
jdk里面的一個可執行程序,jdk的位置:
如何啟動:
1.啟動之前保證idea的程序已經跑起來了
2.有的兄弟需要管理員方式運行(正常情況不行的話)
=>
3.Thread的一些其他方法
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("創建的線程");},"主線程");System.out.println(t.getId()); //java 給這個線程分配的idSystem.out.println(t.getName()); // 我設置的線程名字 -> 主線程System.out.println(t.isDaemon()); // 后臺線程? falseSystem.out.println(t.isAlive());// 線程是否存活 false 還沒開始運行t.setDaemon(true); // 設置t線程為后臺線程//這四行一下就執行完了t.start();System.out.println(t.isAlive()); //true ,不過就存活這一下就結束了,其他線程都結束了//后臺線程肯定結束了// 最后發現沒打印創建的線程}
4.中斷一個線程(interrupt)
java里面就是讓一個線程快點結束(而不是直接讓一個線程中間斷掉(這樣一個線程會導致殘留數據,不太科學))
4.1.手動設置標志位
我們用一個isQuit作為標志位,讓主線程5秒后修改isQuit的值
public static boolean isQuit = false; // 類屬性public static void main(String[] args) throws InterruptedException {//這里就涉及到了變量的捕獲的語法,但是Thread t = new Thread(() ->{while(!isQuit){//我這里捕獲到的其實是拷貝外部變量的一份System.out.println("我新創建的一個線程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("線程工作完畢");},"t線程");t.start();Thread.sleep(5000); // 五秒之后設置isQuit = trueisQuit = true;}
給大家提一個問題,畢竟當時小編在寫這個代碼的時候也忘了
這里不涉及是涉及匿名內部類的變量捕獲嗎?為什么這里不報錯呢?
答:這個時候isQuit是成員變量,此時lambda訪問這個成員,就不再是變量捕獲的語法了,變成了“內部類訪問外部類”的語法,就沒有final(實際上沒有被修改過的也算)的限制了
另外:對于基本數據類型,捕獲的是它們的值的副本(實際上就是復制了一份)
如果是局部變量的話就會報錯,因為修改了
如果一個局部變量不能解決問題就可以考慮把這個局部變量換成成員變量
但是這樣寫還是不太舒服
1.還要我們自己創建
2.并且如果另一個線程正在sleep就不能夠及時響應
java有沒有已經設置好的標志位呢?當然!!!
4.2.Thread內置的標志位
t.interrupt的作用:
public static void main(String[] args) {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("我新建的一個線程");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace(); // 這里扔出一個異常,也僅僅只是打印出異常的位置信息而已,也沒真正的處理什么}}System.out.println("我的循環即將終止");});t.start();try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("讓線程終止");t.interrupt();// 把線程內部的標志位設置為true}
我們發現線程確實還是在運行,一張圖搞懂
還有一個靜態方法,那大家都用的是一個標志位實在是不太科學(但是如果是你想讓兩個東西抵消好像還可以)(先記住吧)
5.線程等待(join方法)
線程等待:一個線程等待另一個線程結束再執行(控制線程結束的順序)
public static void main(String[] args) throws InterruptedException { //線程阻塞拋出的異常Thread t =new Thread(() ->{while(true){System.out.println("我新建的一個線程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();System.out.println("等待線程開始");t.join(5000); // 讓主線程等待5s,也會造成線程阻塞,也要拋出異常System.out.println("join 等待結束");}
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {System.out.println("我新建的一個線程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}});long t1 = System.currentTimeMillis();t.start();t.join(); // 也會造成線程阻塞,也要拋出異常long t2 = System.currentTimeMillis(); // 調度,還有創建線程的開銷System.out.println(t2 - t1); //}
6.線程狀態
public static void main(String[] args) {for(Thread.State state : Thread.State.values()){System.out.println(state);}}
//觀察狀態public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {try {Thread.sleep(1000);System.out.println("11111111");} catch (InterruptedException e) {throw new RuntimeException(e);}});System.out.println(t.getState()); // NEWt.start();System.out.println(t.getState()); //RUNNABLEThread.sleep(500);System.out.println(t.getState()); //TIMED_WAITING: 由于sleep這種固定時間的方式產生的阻塞System.out.println(t.getState()); // terminated,Thread對象還在,但是我創建的對象已經跑完了}
7.線程安全問題(最重要)
簡述:
單個線程下執行可以,但是多個線程執行就出現bug,這種就是線程安全問題
(寫出的代碼和預想的結果不同就是bug!!!)
我們先看一個代碼:兩個線程同時修改一個靜態成員變量
public static int count;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for(int i = 0; i < 5000; i++){count++;}});Thread t2 = new Thread(() ->{for (int i = 0; i < 5000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
竟然不是10000
但是我們修改一下代碼:結果就是10000了
public static int count;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for(int i = 0; i < 5000; i++){count++;}});Thread t2 = new Thread(() ->{for (int i = 0; i < 5000; i++) {count++;}});t1.start();t1.join();t2.start();t2.join();System.out.println(count);}
到底是什么原因呢?
count++這個操作其實是分成三步進行的(CPU通過三個指令來實現的)
這種也可能(所以說這排列組合是無數種)
歸根到底:
這是因為線程的搶占式運行,兩個線程不是往上累加而是獨立運行。
如果這個count++變成一條指令(原子的),那么線程隨便執行都沒問題了
所有的都變成了樣子,這個時候我們就想到給這個count++進行加鎖,把他變成一條指令!!!
8.synchronized(原子性)
我們就需要使用synchronized關鍵字給一個代碼塊進行加鎖
public static int count;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {for(int i = 0; i < 5000; i++){synchronized (locker){count++;}//synchronized,// 我們把要加鎖的代碼放在這個代碼塊里面就行了//我們如果對兩個線程加相同的鎖,就會造成"鎖競爭/鎖沖突“,畢竟我們就只有這一把鎖,線程2必須等線程1解鎖然后再加鎖// 由于我們是對一個變量進行操作,我們還是盡量加同一把鎖,兩把鎖,還是會并發執行//導致:那么一個線程對 count 的修改可能不會被另一個線程立即看到.}});Thread t2 = new Thread(() ->{for (int i = 0; i < 5000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
之后就變成這么執行了,變成了串行執行
t2線程由于鎖的競爭(拿不到locker的監視器鎖)就只能阻塞等待,一直等待到t1線程unlock之后才能獲得這把鎖,這樣就避免了load,add,save的穿插執行。
如果我們兩個線程分別給兩個線程進行加鎖,就還是會穿插執行。因為沒有了鎖競爭!!!
后面的線程安全問題后面再講吧~~~(上一次看鎧甲勇士還是在上一次~~~哈哈哈)
上述就是進程的"黑匣子":PCB如何記錄著任務的一生
的全部內容了,線程的出現,我們的效率又得到了很大的提升~~~,但是管理和安全也是一個很大的問題。預知后事如何,請聽下回分解~~~
能看到這里相信您一定對小編的文章有了一定的認可。
有什么問題歡迎各位大佬指出
歡迎各位大佬評論區留言修正~~