Java——多執行緒(未完)
一、執行緒簡介
-
任務:執行的目標,具體由執行緒來實現
-
程序:程式是指令和資料的有序集合,是一個靜態的概念;程序是執行程式的一次執行過程,是一個動態的概念.(程序是系統資源分配的單位)
-
執行緒:一個程序至少有一個執行緒.執行緒是CPU排程和執行的單位
注:
- 真正的多執行緒是指有多個cpu,即多核,如伺服器.如果是模擬出來的多執行緒,即在一個cpu的情況下,在同一個時間點,cpu只能執行一個程式碼,因為切換的很快,所以有了看似併發的效果
- 程式中的main函式可以理解為主執行緒
- 後臺始終保持了一個gc執行緒(垃圾回收執行緒)
-
多執行緒:多個執行緒的同時執行
二、執行緒建立的方式
-
Thread class:繼承Thread類(其實也是實現了Runnable介面)
- 新建一個類並繼承Thread類
- 重寫run方法,也就是執行緒執行體
- 在主執行緒中new一個該物件,通過start方法執行
package com.guan.test; public class ThreadTeast1 extends Thread{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("次執行緒" + i); } } public static void main(String[] args) { ThreadTeast1 threadTeast1 = new ThreadTeast1(); threadTeast1.start(); for (int i = 0; i < 1000; i++) { System.out.println("主執行緒" + i); } } }
注:執行緒的排程是有延時的,由cpu排程執行.所以這裡的主函式的迴圈建議設為1000(具體由電腦的特性決定),否則無法清晰地看到主執行緒的輸出將次執行緒的輸出包裹
延伸:利用多執行緒下載圖片
匯入相關的依賴:
<!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency>
編寫主程式:
package com.guan.test; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; public class ThreadTest2 extends Thread{ private String url; private String name; public ThreadTest2(){ } public ThreadTest2(String url,String name){ this.url = url; this.name = name; } @Override public void run() { PictureDownloader downloader = new PictureDownloader(); try { downloader.pictureDownloader(this.url,this.name); System.out.println("下載完成:" + name); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { ThreadTest2 threadTest1 = new ThreadTest2("xxx1.png","xxx1.png"); ThreadTest2 threadTest2 = new ThreadTest2("xxx2.jpg","xxx2.jpg"); ThreadTest2 threadTest3 = new ThreadTest2("xxx3.png","xxx3.png"); threadTest1.start(); threadTest2.start(); threadTest3.start(); } } class PictureDownloader{ public void pictureDownloader(String url,String name) throws IOException { //呼叫了外部的圖片下載工具 FileUtils.copyURLToFile(new URL(url),new File(name)); } }
注:這裡實現的一個有趣的細節,外部的資料需要作為Thread的實現類的屬性放進去!
圖片大小:1>3>2
現在完成速度:3>2>1
-
Runnable介面:實現Runnable介面
靜態代理
-
定義類實現Runnable介面
-
實現run()方法,編寫執行緒執行體
-
建立Thread物件,同時將Runnable實現類的物件丟進去,呼叫start()方法啟動執行緒
package com.guan.test; public class ThreadTest3 implements Runnable{ public void run() { for (int i = 0; i < 20; i++) { System.out.println("次執行緒" + i); } } public static void main(String[] args) { ThreadTest3 threadTest3 = new ThreadTest3(); new Thread(threadTest3).start(); for (int i = 0; i < 1000; i++) { System.out.println("主執行緒" + i); } } }
將上一個圖片下載的類改成用Runnable實現
package com.guan.test; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; public class ThreadTest2 implements Runnable{ private String url; private String name; public ThreadTest2(){ } public ThreadTest2(String url,String name){ this.url = url; this.name = name; } public void run() { PictureDownloader downloader = new PictureDownloader(); try { downloader.pictureDownloader(this.url,this.name); System.out.println("下載完成:" + name); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { ThreadTest2 threadTest1 = new ThreadTest2("http://icwl.org.cn/images/sponsors1.png","sponsors1.png"); ThreadTest2 threadTest2 = new ThreadTest2("http://icwl.org.cn/images/sponsors2.jpg","sponsors2.jpg"); ThreadTest2 threadTest3 = new ThreadTest2("http://icwl.org.cn/images/sponsors3.png","sponsors3.png"); //主要是這裡用了靜態代理方式的實現 new Thread(threadTest1).start(); new Thread(threadTest2).start(); new Thread(threadTest3).start(); } } class PictureDownloader{ public void pictureDownloader(String url,String name) throws IOException { FileUtils.copyURLToFile(new URL(url),new File(name)); } }
靜態代理的實現:(從三個物件說起)
- 抽象角色:真實角色實現的業務,通常為介面,且被真實角色和代理角色實現(其實代理角色實現的介面就是與之相對應的真實角色的介面,當然可能還增加了一些業務).Runable介面,主要是run方法的實現(Thread類中的Runnable型別的欄位——target)
- 真實角色:ThreadTest3類,也就是被代理的物件(Thread中target屬性的例項)
- 代理角色:Thread類,代理了ThreadTest3要實現的操作,同時添加了一些附屬的操作,比如開啟了一個新的執行緒進行代理
優點:避免了單繼承的侷限性,靈活方便,同一個物件,可以被多個執行緒使用
缺點:在使用同一個物件的情況下,可能多個執行緒操作同一個資源造成執行緒不安全
package com.guan.test; public class ThreadTest4 implements Runnable { private int resources = 10; public void run() { while(true){ //獲得當前執行緒的名字 System.out.println(Thread.currentThread().getName() + "搶到了第" + resources-- + "張票"); try { //小明的執行緒第一個開啟,為了防止他一下子搶完票,每次先睡0.2s Thread.currentThread().sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } if (resources <=1){ break; } } } public static void main(String[] args) { ThreadTest4 test4 = new ThreadTest4(); new Thread(test4,"小明").start(); new Thread(test4,"老師").start(); new Thread(test4,"黃牛黨").start(); } }
注:幾個人之間很可能搶到相同的一張票
-
-
Callable介面:實現Callable介面
- 實現Callable介面,需要返回值型別(介面的返回值????)
- 重寫call方法,需要丟擲異常
- 建立目標物件
- 建立執行服務:
ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交執行:
Future<Boolean> result1 = ser.submit(t1);
- 獲取結果:
boolean r1 = result1.get();
- 關閉服務:
ser.shutdownNow();
優點:
- 可以定義返回值
- 可以丟擲異常
初次使用:(後面還會詳細講到Executor類)
package com.guan.test; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.concurrent.*; public class ThreadTest6 implements Callable<Boolean> { private String url; private String name; public ThreadTest6(){ } public ThreadTest6(String url,String name){ this.url = url; this.name = name; } //該方法的返回值為布林型 public Boolean call() { PictureDownloader downloader = new PictureDownloader(); try { downloader.pictureDownloader(this.url,this.name); System.out.println("下載完成:" + name); } catch (IOException e) { e.printStackTrace(); return false; } return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { ThreadTest6 threadTest1 = new ThreadTest6("http://icwl.org.cn/images/sponsors1.png","sponsors1.png"); ThreadTest6 threadTest2 = new ThreadTest6("http://icwl.org.cn/images/sponsors2.jpg","sponsors2.jpg"); ThreadTest6 threadTest3 = new ThreadTest6("http://icwl.org.cn/images/sponsors3.png","sponsors3.png"); //建立連線池 ExecutorService ser = Executors.newFixedThreadPool(3); //可以建立的執行緒數量為3 //提交執行緒 Future<Boolean> result1 = ser.submit(threadTest1); Future<Boolean> result2 = ser.submit(threadTest2); Future<Boolean> result3 = ser.submit(threadTest3); boolean r1 = result1.get(); boolean r2 = result2.get(); boolean r3 = result3.get(); System.out.println(r1); System.out.println(r2); System.out.println(r3); //關閉連線池 ser.shutdownNow(); } }
這裡看一下Callable介面中的call方法
@FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
注:v是call的返回值.當我們實現這一介面時,需要將v的型別寫在介面的附近(如上例中的返回型別就是Boolean):
public class ThreadTest6 implements Callable<Boolean>
三、Lambda表示式(函數語言程式設計)
使用前提:函式式介面
函式式介面的定義:
- 任何介面,如果只包含唯一一個抽象方法,那它就是一個函式式介面
- 對於函式式介面,我們可以通過lambda表示式來建立該介面的物件
優化過程:內部實現類->靜態內部類->區域性內部類->匿名內部類(沒有類的名稱,必須藉助**介面**或者父類)->lambda表示式簡化(可以簡化引數型別,括號,花括號)
程式碼:
package com.guan.test;
public class ThreadTest7 {
//靜態內部類
static class ILove2 implements Love{
public void ILove() {
System.out.println("I love 2");
}
}
public static void main(String[] args) {
//區域性內部類
class ILove3 implements Love{
public void ILove() {
System.out.println("I love 3");
}
}
ILove1 iLove1 = new ILove1();
ILove2 iLove2 = new ILove2();
ILove3 iLove3 = new ILove3();
//匿名內部類
Love iLove4 = new Love(){
public void ILove() {
System.out.println("I love 4");
}
}; //這是個語句,所以需要分號作為結尾
//箭頭函式
Love iLove5 = ()->{
System.out.println("I love 5");
};
iLove1.ILove();
iLove2.ILove();
iLove3.ILove();
iLove4.ILove();
iLove5.ILove();
}
}
//函式式介面
interface Love{
public abstract void ILove();
}
//內部實現類
class ILove1 implements Love{
public void ILove() {
System.out.println("I love 1");
}
}
優點:
- 避免匿名內部類定義過多
- 讓程式碼看上去更加簡潔
- 去掉一堆沒有意義的程式碼,只留下核心的邏輯
應用場景:多執行緒中的Runnable介面
注:在idea中的使用需要進行一些設定,詳細可見這篇文章https://blog.csdn.net/mtngt11/article/details/100052996
四、執行緒狀態
狀態:建立,就緒,執行,阻塞,死亡
相關方法:
- setPriority:更改執行緒的優先順序
- sleep:讓當前正在執行的執行緒休眠
- join:等到該執行緒終止
- yield:暫停當前正在執行的執行緒物件,並執行其它執行緒
- interrupt:中斷執行緒
- isAlive:測試執行緒是否處於活動狀態
停止執行緒:
-
建議執行緒正常停止:利用次數,不推薦死迴圈
-
建議使用一個標誌位(flag),比如保持執行緒是在
flag=true
的情況下才能執行 -
不要用過時或者JDK不建議使用的方法
package com.guan.test; public class ThreadTest8 implements Runnable { private Boolean flag = true; @Override public void run() { int i=0; while(flag){ System.out.println("次執行緒正在跑" + ++i); } } public void stop(){ this.flag=false; } public static void main(String[] args) { ThreadTest8 thread = new ThreadTest8(); new Thread(thread).start(); for (int i = 0; i < 1000; i++) { System.out.println("主執行緒正在跑" + i); if (i == 900){ thread.stop(); System.out.println("次執行緒停下了!"); } } } }
-
執行緒休眠
特點:
- sleep(時間)指定當前執行緒阻塞的毫秒數
- sleep存在異常InterruptedException
- sleep時間達到後執行緒進入就緒狀態
- sleep可以模擬網路延時,倒計時等
- 每一個物件都有一個鎖,sleep不會釋放鎖
作用:
-
模擬網路延時,放大問題的發生性
package com.guan.test; public class ThreadTest4 implements Runnable { private int resources = 10; public void run() { while(true){ System.out.println(Thread.currentThread().getName() + "搶到了第" + resources-- + "張票"); try { //小明的執行緒第一個開啟,為了防止他一下子搶完票,每次先睡0.2s Thread.currentThread().sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } if (resources <=1){ break; } } } public static void main(String[] args) { ThreadTest4 test4 = new ThreadTest4(); new Thread(test4,"小明").start(); new Thread(test4,"老師").start(); new Thread(test4,"黃牛黨").start(); } }
注:還是這個案例,如果不睡一下,所有票都會被小明搶到,因為小明這個執行緒是最先開啟的,且搶票太快了.如果睡一下,又會發現執行緒是不安全的,因為有多個人很可能正好搶到了同一張票
-
模擬倒計時,列印當前時間(如果採用的不是主執行緒可能無法拿到控制權,因此這個方案是不太嚴謹的)
package com.guan.test; import java.text.SimpleDateFormat; import java.util.Date; public class ThreadTest9 { public static void TenDown() throws InterruptedException { int now = 10; while(now>0){ System.out.println(now--); //這裡相當於睡的是主執行緒 Thread.sleep(1000); } } public static void logTime() throws InterruptedException { while(true){ Date date = new Date(System.currentTimeMillis()); System.out.println(new SimpleDateFormat("HH:mm:ss").format(date)); Thread.sleep(1000); } } public static void main(String[] args) throws InterruptedException { ThreadTest9.TenDown(); } }
-
執行緒休眠
特點:
-
禮讓執行緒,讓當前正在執行的執行緒暫停,但不阻塞
-
將執行緒從執行狀態轉為就緒狀態
-
讓cpu重新排程,禮讓不一定成功,和具體的CPU排程演算法有關
一個不太嚴謹的測試:
package com.guan.test; public class ThreadTest10 implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "執行緒開始執行"); Thread.yield(); System.out.println(Thread.currentThread().getName() + "執行緒結束執行"); } public static void main(String[] args) { ThreadTest10 test10 = new ThreadTest10(); new Thread(test10,"a").start(); new Thread(test10,"b").start(); } }
-
-
合併程序
特點:
-
Join合併繼承,待此程序執行完成後,再執行其他執行緒,其他執行緒阻塞
package com.guan.test; public class ThreadTest11 implements Runnable { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("次執行緒在跑:" + i); } } public static void main(String[] args) throws InterruptedException { ThreadTest11 test11 = new ThreadTest11(); Thread thread = new Thread(test11); thread.start(); for (int i = 0; i < 1000; i++) { System.out.println("主執行緒在跑:" + i); if(i==500){ //在副執行緒跑的過程中,讓位給主執行緒跑 thread.join(); } } } }
-
-
觀測執行緒的狀態
package com.guan.test; public class ThreadTest12 implements Runnable{ @Override public void run() { System.out.println("執行緒開始了"); for (int i = 0; i < 5; i++) { try { // System.out.println("睡"); Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("執行緒結束"); } public static void main(String[] args) throws InterruptedException { ThreadTest12 test12 = new ThreadTest12(); Thread thread = new Thread(test12); Thread.State state = thread.getState(); System.out.println(state); //new thread.start(); state = thread.getState(); //runnable while (state != Thread.State.TERMINATED){ state = thread.getState(); //time_waiting System.out.println(state); Thread.sleep(100); } System.out.println(state); //terminated thread.start(); } }
注:
- 死亡的執行緒無法再次啟動
- 下面輸出的間隔需要定位100,如果定位1000很可能兩邊間隔相重疊導致輸出Runable
-
執行緒優先順序
-
特點:Java提供一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有程序,執行緒排程器按照優先順序決定應該排程哪個執行緒來執行
-
優先順序範圍:1~10
-
注意執行緒的實際執行還是要看cpu,但是優先順序搞得執行緒執行的可能性更大
package com.guan.test; public class ThreadTest13 extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority()); } public static void main(String[] args) { System.out.println("main --> " + Thread.currentThread().getPriority()); ThreadTest13 test1 = new ThreadTest13(); ThreadTest13 test2 = new ThreadTest13(); ThreadTest13 test3 = new ThreadTest13(); ThreadTest13 test4 = new ThreadTest13(); ThreadTest13 test5 = new ThreadTest13(); ThreadTest13 test6 = new ThreadTest13(); test1.setPriority(-1); test2.setPriority(2); test3.setPriority(4); test4.setPriority(6); test5.setPriority(8); test6.setPriority(11); test1.start(); test2.start(); test3.start(); test4.start(); test5.start(); test6.start(); } }
注:先設定優先順序再啟動
-
-
守護執行緒(daemon)
特點:
- 執行緒分為使用者執行緒和守護執行緒
- 虛擬機器必須確保使用者執行緒執行完畢
- 虛擬機器不用等待守護執行緒執行完畢(可以認為,守護執行緒隨著使用者執行緒的消亡而消亡)
- 如:後臺記錄操作日誌,監控記憶體,垃圾回收等
程式碼:
package com.guan.test; public class ThreadTest14 { public static void main(String[] args) { God god = new God(); People people = new People(); Thread thread = new Thread(god); thread.setDaemon(true); //設定為守護執行緒 thread.start(); new Thread(people).start(); } } class God implements Runnable{ @Override public void run() { while(true){ System.out.println("God bless you!"); } } } class People implements Runnable{ @Override public void run() { for (int i = 0; i < 36500; i++) { System.out.println("活著的第" + i + "天"); } } }
注:可以看到雖然守護執行緒永遠為true,但是在使用者結束後它也會結束.簡單來說就是把你送走了它才走
五、執行緒同步機制
併發:同一個物件被多個執行緒同時操作
執行緒同步:執行緒同步多用於處理多執行緒問題(通常為多個執行緒訪問同一個物件).執行緒同步其實是一種等待機制,多個需要同時訪問此物件的執行緒進入這個物件的等待池形成佇列,等待前面執行緒使用完畢,下一個執行緒再使用
每個執行緒在自己的工作記憶體互動,記憶體控制不當會造成資料不一致(第一個執行緒的工作記憶體有3張票,第二個執行緒的工作記憶體有5張票,而實際上可能已經沒有票了,這種情況下的售票顯然會得到負值)
兩個測試案例:
package com.guan.test;
public class ThreadTest15 implements Runnable{
private int num = 10;
private Boolean flag =true;
@Override
public void run() {
while(flag){
buy();
}
}
private void buy() {
if (num>0){
//在讀與寫之間增加延時可以放大效果
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + num-- + "張票");
}else{
flag = false;
}
}
public static void main(String[] args) {
ThreadTest15 test = new ThreadTest15();
new Thread(test,"1").start();
new Thread(test,"2").start();
new Thread(test,"3").start();
}
}
package com.guan.test;
public class ThreadTest16 implements Runnable{
Acount acount;
int now;
int get;
public ThreadTest16(Acount acount, int now, int get) {
this.acount = acount;
this.now = now;
this.get = get;
}
@Override
public void run() {
if(acount.num-get>=0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
acount.num -= get;
now += get;
System.out.println(Thread.currentThread().getName() + "現在有:" + now + ";acount現在還有" + acount.num);
}else{
System.out.println(Thread.currentThread().getName() + "取不出:" + get);
}
}
public static void main(String[] args) {
Acount acount = new Acount("基金", 100);
ThreadTest16 threadTest1 = new ThreadTest16(acount,100,50);
ThreadTest16 threadTest2 = new ThreadTest16(acount,100,100);
new Thread(threadTest1,"小明").start();
new Thread(threadTest2,"小紅").start();
}
}
class Acount{
String name;
int num;
public Acount(String name, int num) {
this.name = name;
this.num = num;
}
}
/*
小明現在有:150;acount現在還有50
小紅現在有:200;acount現在還有-50
原因:小明差->sleep->小紅查->sleep->小明釦->小紅扣
*/
/*
小紅現在有:200;acount現在還有0
小明現在有:150;acount現在還有0
看上去很離譜的答案:小紅查且取到100資料->小明查且取到100資料->小明付款且返回50->小紅付款且返還0(覆蓋50)->兩個人都打印出了0(50同樣的道理,雖然還是很離譜,但是仔細想想所有的程式都是要編譯成小段的機器碼執行也就稍微可以理解了)
*/
注:通過這裡0/0,50/50的結果我們可以極大地感受到執行緒的不安全性
執行緒不安全的集合:
-
ArrayList (兩個執行緒再同一時間覆蓋了同一位置,倒置出現數據的缺失)
package com.guan.test; import java.util.ArrayList; public class ThreadTest17 { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { new Thread(()->{ list.add(3); }).start(); } System.out.println(list.size()); } }
注:這裡實際的list.size不會有1000,因為線上程運作的過程中很可能出現兩個執行緒共同只能用同一個位置,從而導致結果錯誤
解決方案:同步方法——synchronized修飾符
本質:佇列+鎖
優點:安全
缺點:效能以及效能倒置問題(優先順序高的執行緒等待優先順序低的執行緒拿到的鎖)
使用:
-
同步方法:修飾方法,但鎖的是方法的this物件
package com.guan.test; public class ThreadTest15 implements Runnable{ private int num = 10; private Boolean flag =true; @Override public void run() { while(flag){ buy(); } } private synchronized void buy() { if (num>0){ //在讀與寫之間增加延時可以放大效果 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "拿到了第" + num-- + "張票"); }else{ flag = false; } } public static void main(String[] args) { ThreadTest15 test = new ThreadTest15(); new Thread(test,"1").start(); new Thread(test,"2").start(); new Thread(test,"3").start(); } }
-
同步塊:修飾語句塊,鎖的物件就是變化的量,需要增刪改的物件
格式:synchronized(obj){}
注:obj是同步監視器,通常是共享資源,如果synchronized修飾的是方法,可以認為obj就是this