推薦學習:《》
程式(program):是為完成特定任務、用某種語言編寫的一組指令的集合,是一段靜態的程式碼。 (程式是靜態的)
程序(process):是程式的一次執行過程,正在執行的一個程式,程序作為資源分配的單位,在記憶體中會為每個程序分配不同的記憶體區域。 (程序是動態的)是一個動的過程 ,程序的生命週期 : 有它自身的產生、存在和消亡的過程
目前作業系統都是支援多程序,可以同時執行多個程序,通過程序ID區分
執行緒(thread):程序中的一條執行路徑,也是CUP的基本排程單位,一個程序由一個或多個執行緒組成,彼此間完成不同的工作,多個執行緒同時執行,稱為多執行緒。
執行緒的組成
任何一個執行緒都具有的基本組成部分:
- CPU時間片:作業系統(OS)會為每一個執行緒分配執行時間。
- 執行資料:堆空間(儲存執行緒需要使用的物件,多個執行緒可以共用堆中的物件);棧空間(儲存執行緒需要使用的區域性變數,每個執行緒都擁有獨立的棧)
執行緒的特點
- 執行緒搶佔式執行(效率高、可防止單一執行緒長時間獨佔CPU)
- 單核CPU在執行的時候,是按照時間片執行的,一個時間片只能執行一個執行緒,因為時間片特別的短,我們感受到的就是「同時」進行的。
- 多核CPU真正意義上做到了一個時間片多個執行緒同時進行
- 在單核CPU中,宏觀上同時進行,微觀上順序執行
- 程序是作業系統中資源分配的基本單位,而執行緒是CPU的基本排程單位
- 一個程式執行後至少有一個程序
- 一個程序可以包含多個執行緒,但是至少需要有一個執行緒,否則這個程序是沒有意義的
- 程序間不能共用資料段地址,但通程序的執行緒之間可以。
1.繼承Thread類
2.重寫run()方法
3.建立子類物件
4.呼叫start()方法(PS:不要呼叫run()方法,這樣相當於普通呼叫物件的方法,併為啟動執行緒)
繼承類
public class MyThread extends Thread { @Override public void run() { for (int i = 1; i <= 50; i++) { System.out.println("子執行緒:==>" + i); } }}
測試類
public class TestThread { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); for (int i = 1; i <= 50; i++) { System.out.println("主執行緒:-->"+i); } }}
結果
getId()
//獲取執行緒的id,每個執行緒都有自己的idgetName()
//獲取執行緒名字Thread.currentThread()
//獲取當前執行緒
程式碼
public class TestThread { public static void main(String[] args) { MyThread t=new MyThread(); t.start(); //只能在繼承Thread類的情況下用 System.out.println("執行緒id:"+t.getId()); System.out.println("執行緒名字:"+t.getName()); //呼叫Thread類的靜態方法獲取當前執行緒(這裡獲取的是主執行緒) System.out.println("執行緒id:"+Thread.currentThread().getId()); System.out.println("執行緒名字:"+Thread.currentThread().getName()); }}
只能修改執行緒的名稱,不能修改執行緒的id(id是由系統自動分配)
1、使用執行緒子類的構造方法賦值
2、呼叫執行緒物件的setName()
方法
程式碼
public class MyThread extends Thread{ public MyThread() {}//無參構造器 public MyThread(String name) { super(name); } public void run() { for(int i=1;i<=50;i++) {} }}
public class TestThread { public static void main(String[] args) { MyThread t1=new MyThread("子執行緒1");//通過構造方法 MyThread t2=new MyThread(); t2.setName("子執行緒2"); System.out.println("執行緒t1的id:"+t1.getId()+" 名稱:"+t1.getName()); System.out.println("執行緒t2的id:"+t2.getId()+" 名稱:"+t2.getName()); }}
1.實現
Runnable
介面
2.實現run()
方法
3.建立實現類物件
4.建立執行緒類物件
5.呼叫start()
方法
實現介面
public class MyRunnable implements Runnable{//實現介面 @Override public void run() {//實現run方法 // TODO Auto-generated method stub for(int i=1;i<=10;i++) { System.out.println("子執行緒:"+i); } }}
測試類
public class TestRunnable { public static void main(String[] args) { //1.建立MyRunnable物件,表示執行緒執行的功能 Runnable runnable=new MyRunnable(); //2.建立執行緒物件 Thread th=new Thread(runnable); //3.啟動執行緒 th.start(); for(int i=1;i<=10;i++) { System.out.println("主執行緒:"+i); } }}
如果一個執行緒方法我們只使用一次,那麼就不必設定一個單獨的類,就可以使用匿名內部類實現該功能
public class TestRunnable { public static void main(String[] args) { Runnable runnable=new Runnable() { @Override public void run() { // TODO Auto-generated method stub for(int i=1;i<=10;i++) { System.out.println("子執行緒:"+ i); } } }; Thread th=new Thread(runnable); th.start(); }}
對比繼承
Thread
類和實現Runnable
介面建立執行緒的方式發現,都需要有一個run()
方法,但是這個run()方法有不足:
- 沒有返回值
- 不能丟擲異常
基於上面的兩個不足,在JDK1.5以後出現了第三種建立執行緒的方式:實現
Callable
介面實現
Callable
介面的好處:
- 有返回值
- 能丟擲異常
缺點:
- 建立執行緒比較麻煩
1.實現
Callable
介面,可以不帶泛型,如果不帶泛型,那麼call方法的返回值就是Object
型別2.如果帶泛型,那麼call的返回值就是泛型對應的型別
3.從call方法看到:方法有返回值,可以丟擲異常
實現介面
import java.util.Random;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class TestCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { // TODO Auto-generated method stub return new Random().nextInt(10); }}
測試類
import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class Test { public static void main(String[] args) throws InterruptedException, ExecutionException { TestCallable tc=new TestCallable(); FutureTask<Integer> ft=new FutureTask<>(tc); //建立執行緒物件 Thread th=new Thread(ft); th.start(); //獲取執行緒得到的返回值 Integer In=ft.get(); System.out.println(In); }}
休眠(當前執行緒主動休眠millis毫秒)
public static void sleep(long millis)
放棄(當前執行緒主動放棄時間片,回到就緒狀態,競爭下一次時間片)
public static void yield()
加入(當一個執行緒呼叫了join方法,這個執行緒就會先被執行,它執行結束以後才可以去執行其餘的執行緒)
public final void join()
//必須先start(),在join(),才有效優先順序(執行緒優先順序為1–10,預設為5,優先順序越高,表示獲取CPU機會越多)
執行緒物件.setPriority()
守護執行緒
- 執行緒物件.setDaemon(true);設定為守護執行緒
- 執行緒有兩類:使用者執行緒(前臺執行緒)、守護執行緒(後臺執行緒)
- 如果程式中所有前臺執行緒都執行完畢了,後臺執行緒也會自動結束
- 垃圾回收器執行緒屬於守護執行緒
public static void sleep(long millis)
當前執行緒主動休眠millis毫秒
子執行緒
public class SleepThread extends Thread{ @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName()+":"+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }}
PS:sleep()的異常在run方法中是不能丟擲的,只能用try–catch處理
測試類
public class Test01 { public static void main(String[] args) { SleepThread sleepThread = new SleepThread(); sleepThread.start(); }}
結果:每次間隔100ms輸出一次
public static void yield()
當前執行緒主動放棄時間片,回到就緒狀態,競爭下一次時間片
子執行緒
public class YieldThread extends Thread{ @Override public void run() { for (int i=1;i<=10;i++){ System.out.println(Thread.currentThread().getName()+":"+i); Thread.yield();//主動放棄資源 } }}
測試類
public class Test01 { public static void main(String[] args) { YieldThread yieldThread01 = new YieldThread(); YieldThread yieldThread02 = new YieldThread(); yieldThread01.start(); yieldThread02.start(); }}
結果:基本都會交替進行,也會有一個執行緒連輸出
當一個執行緒呼叫了join方法,這個執行緒就會先被執行,它執行結束以後才可以去執行其餘的執行緒,必須先start,再join才有效
子執行緒
public class JoinThread extends Thread{ @Override public void run() { for (int i=1;i<=10;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } }}
測試類
public class Test01 { public static void main(String[] args) throws InterruptedException { for (int i=1;i<=10;i++){ System.out.println(Thread.currentThread().getName()+":"+i); if(i==5){ JoinThread joinThread = new JoinThread(); joinThread.start(); joinThread.join(); } } }}
結果:當主執行緒列印到5的時候,這時候子執行緒加入進來,就先執行完子執行緒,在執行主執行緒
將子執行緒設定為主執行緒的伴隨執行緒,主執行緒停止的時候,子執行緒也不要繼續執行了
注意:先設定,在啟動
子執行緒
public class TestThread extends Thread{ @Override public void run() { for(int i=1;i<=1000;i++){ System.out.println(Thread.currentThread().getName()+":"+i); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } }}
測試類
public class Test01 { public static void main(String[] args) throws InterruptedException { TestThread daemonThread = new TestThread(); daemonThread.setDaemon(true);//設定守護執行緒 daemonThread.start(); for (int i=1;i<=10;i++){ System.out.println(Thread.currentThread().getName()+":"+i); Thread.sleep(100); } }}
結果:當主執行緒結束時,子執行緒也跟著結束,並不會繼續執行下去列印輸出
執行緒優先順序為1-10,預設為5,優先順序越高,表示獲取CPU機會越多
子執行緒
public class TestThread extends Thread{ @Override public void run() { for(int i=1;i<=100;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } }}
測試
public class Test01 { public static void main(String[] args) throws InterruptedException { TestThread th1 = new TestThread(); TestThread th2 = new TestThread(); TestThread th3 = new TestThread(); th1.setPriority(10);//設定執行緒1優先順序10 th1.start(); th2.start();//執行緒2優先順序預設不變,為5 th3.setPriority(1);//設定執行緒3優先順序為1 th3.start(); }}
結果:優先順序(th1>th2>th3)執行緒3應該在最後列印
需求:模擬三個視窗,每個視窗有100個人,同時搶10張票
使用繼承Runnable介面的方法
public class BuyTicketRunnable implements Runnable{ private int ticketNum=10; @Override public void run() { for(int i=1;i<=100;i++) { if(ticketNum<=0) break; System.out.println("在"+Thread.currentThread().getName()+"買到了第"+ticketNum+"張票!"); ticketNum--; } }}
測試方法
public class BuyTicketTest { public static void main(String[] args) { // TODO Auto-generated method stub Runnable runnable=new BuyTicketRunnable(); Thread th1=new Thread(runnable,"視窗1"); Thread th2=new Thread(runnable,"視窗2"); Thread th3=new Thread(runnable,"視窗3"); th1.start(); th2.start(); th3.start(); }}
結果
我們發現,不同視窗會搶到同一張票!!!,這在實際情況是不允許的,這是因為多個執行緒,在爭搶資源的過程中,導致共用的資源出現問題。一個執行緒還沒執行完,另一個執行緒就參與進來了,開始爭搶。(但視窗2搶到第10張票,還沒來得及
ticketNum--
操作,時間片就用完了,隨後被視窗三搶到CPU資源,此時的票數還是10,視窗三也搶到第十張票,也還沒來得及ticketNum--
操作視窗三時間片由完了,視窗一搶到CPU資源,還是買到了第10張票)
多執行緒安全問題:
- 當多執行緒並行存取臨界資源時,如果破壞原子操作,可能會造成資料不一致
- 臨界資源:共用資源(同一物件),一次只能允許一個執行緒使用,才可以保證其正確性
- 原子操作:不可分割的多步操作,被視作一個整體,其順序和步驟不可被打亂或預設
synchronized
(同步監視器)
- 必須是參照資料型別,不能是基本資料型別
- 也可以建立一個專門的同步監視器,沒有任何業務含義 (new Object)
- 一般使用共用資源做同步監視器即可
- 在同步程式碼塊中不能改變同步監視器物件的參照
- 儘量不要String和包裝類Integer做同步監視器,建議使用final修飾同步監視器
對賣票案例改進
public class BuyTicketRunnable implements Runnable{ static Object obj=new Object(); private int ticketNum=10; @Override public void run() { for(int i=1;i<100;i++) { //把具有安全隱患的程式碼鎖住即可,如果鎖多了就會效率低 synchronized (obj) {//鎖必須多個執行緒用的是同一把鎖!!也可以使用this,表示的是該物件本身 System.out.println("在"+Thread.currentThread().getName()+"買到了第"+ticketNum+"張票!"); ticketNum--; } } }}
- 多個程式碼塊使用了同一個同步監視器(鎖),鎖住一個程式碼塊的同時,也鎖住所有使用該鎖的所有程式碼塊,其他執行緒無法存取其中的任何一個程式碼塊
- 多個程式碼塊使用了同一個同步監視器(鎖),鎖住一個程式碼塊的同時,也鎖住所有使用該鎖的所有程式碼塊, 但是沒有鎖住使用其他同步監視器的程式碼塊,其他執行緒有機會存取其他同步監視器的程式碼塊
synchronized
(同步方法)
- 不要將run()定義為同步方法
- 非靜態同步方法的同步監視器是this;靜態同步方法(static)的同步監視器是 類名.class 位元組碼資訊物件
- 同步程式碼塊的效率要高於同步方法(原因:同步方法是將執行緒擋在了方法的外部,而同步程式碼塊鎖將執行緒擋在了程式碼塊的外部,但是卻是方法的內部)
- 同步方法的鎖是this,一旦鎖住一個方法,就鎖住了所有的同步方法;同步程式碼塊只是鎖住使用該同步監視器的程式碼塊,而沒有鎖住使用其他監視器的程式碼塊
買票案例改進
public class BuyTicketRunnable implements Runnable{ private int ticketNum=10; @Override public void run() { for(int i=1;i<100;i++) { BuyTicket(); } } public synchronized void BuyTicket() {//鎖住的是:this,如果是靜態方法:當前類.class if(ticketNum>0) { System.out.println("在"+Thread.currentThread().getName()+"買到了第"+ticketNum+"張票!"); ticketNum--; } }}
Lock
鎖:
- DK1.5後新增新一代的執行緒同步方式:Lock鎖,與採用synchronized相比,lock可提供多種鎖方案,更靈活
- synchronized是Java中的關鍵字,這個關鍵字的識別是靠JVM來識別完成的呀。是虛擬機器器級別的。
但是Lock鎖是API級別的,提供了相應的介面和對應的實現類,這個方式更靈活,表現出來的效能優於之前的方式。
對買票案例改進
import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class BuyTicketRunnable implements Runnable{ private int ticketNum=10; Lock lock=new ReentrantLock();//介面=實現類 可以使用不同的實現類 @Override public void run() { for(int i=1;i<100;i++) { lock.lock();//開啟鎖 try { if(ticketNum>0) { System.out.println("在"+Thread.currentThread().getName()+"買到了第"+ticketNum+"張票!"); ticketNum--; } }catch(Exception e) { e.printStackTrace(); }finally { //關閉鎖:--->即使有異常,這個鎖也可以得到釋放 lock.unlock(); } } }}
Lock和synchronized的區別
- Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖
- Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充套件性(提供更多的子類)
- 不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖
- 出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續
*案例:男孩女孩一起去吃飯,但是桌子上只有兩根筷子,如果兩個人同時搶到一根筷子而不放棄,這樣兩個人都吃不上飯,這樣就形成死鎖了;必須要有一個人放棄爭搶,等待另一個人用完,釋放資源,這個人之後才會獲得兩根筷子,兩個人才能都吃上飯 *
package 多執行緒;class Eat{ //代表兩個筷子 public static Object o1=new Object(); public static Object o2=new Object(); public static void eat() { System.out.println("可以吃飯了"); }}class BoyThread extends Thread{ public void run() { synchronized (Eat.o1) { System.out.println("男孩拿到了第一根筷子!"); synchronized (Eat.o2) { System.out.println("男孩拿到了第二根筷子!"); Eat.eat(); } } }}class GirlThread extends Thread{ public void run() { synchronized (Eat.o2) { System.out.println("女孩拿到了第二根筷子!"); synchronized (Eat.o1) { System.out.println("女孩拿到了第一根筷子!"); Eat.eat(); } } }}public class MyLock { public static void main(String[] args) { BoyThread boy=new BoyThread(); GirlThread girl=new GirlThread(); boy.start(); girl.start(); }}
結果
解決辦法
先讓男孩拿到筷子,執行緒休眠一下,等待男孩用完筷子,在啟動女孩執行緒
public class MyLock { public static void main(String[] args) { BoyThread boy=new BoyThread(); GirlThread girl=new GirlThread(); boy.start(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } girl.start(); }}
在寫程式中要避免這種死鎖:減少同步資源的定義,避免巢狀同步
在Java物件中,有兩種池
- 鎖池(
synchronized
)- 等待池(
wait()
;notify()
;notifyAll()
)
如果一個執行緒呼叫了某個物件的wait方法,那麼該執行緒進入到該物件的等待池中(並且已經將鎖釋放);
如果未來的某個時刻,另外一個執行緒呼叫了相同的物件notify方法或者notifyAll方法,那麼該等待池中的執行緒就會被喚醒,然後進入到物件的鎖池裡面去獲得該物件的鎖;
如果獲得鎖成功後,那麼該執行緒就會沿著wait方法之後的路徑繼續執行。注意:沿著wait方法之後執行
- wait():的作用是讓當前執行緒進入等待狀態,同時,wait()也會讓當前執行緒釋放它所持有的鎖。直到其他執行緒呼叫此物件的
notify()
方法或notifyAll()
方法,當前執行緒被喚醒(進入就緒狀態)- wait(long timeout):讓當前執行緒處於「等待(阻塞)狀態,直到其他執行緒呼叫此物件的
notify()
方法或notifyAll()
方法,或者超過指定的時間量」,當前執行緒被喚醒(進入就緒狀態)
sleep和wait的區別:sleep進入阻塞狀態沒有釋放鎖,wait進入阻塞狀態但是同時釋放了鎖
notify()和notifyAll()的作用,則是喚醒當前物件上的等待執行緒
- notify()是喚醒單個執行緒
- notifyAll()是喚醒所有的執行緒
案例:
假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中產品取走消費。
如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走為止。
如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止。
功能分解一:商品類
public class Product {//商品類 private String name;//名字 private String brand;//品牌 boolean flag = false;//設定標記,false表示商品沒有,等待生產者生產 public synchronized void setProduct(String name, String brand) {//生產商品,同步方法,鎖住的是this if (flag == true) {//如果flag為true,代表有商品,不生產,等待消費者消費 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //生產商品 this.setName(name); this.setBrand(brand); System.out.println("生產者生產了" +this.getBrand() +this.getName()); //生產完,設定標誌 flag = true; //喚醒消費執行緒 notify(); } public synchronized void getProduct() { if (flag == false) {//如果是false,則沒有商品,等待生產者生產 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果有商品,消費 System.out.println("消費者消費了" + this.getBrand() +this.getName()); //設定標誌 flag = false; //喚醒執行緒 notify(); } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setBrand(String brand) { this.brand = brand; } public String getBrand() { return brand; }}
功能分解二:生產者執行緒
public class ProducterThread extends Thread {//生產者執行緒 private Product p; public ProducterThread(Product p) { this.p = p; } @Override public void run() { for (int i = 1; i <= 10; i++) { if(i%2==0){//如果是奇數,就生產巧克力,如果是偶數,就生產方便麵 p.setProduct("巧克力","德芙"); }else{ p.setProduct("方便麵","康師傅"); } } }}
功能分解三:消費者執行緒
public class CustomerThread extends Thread {//消費者執行緒 private Product pro; public CustomerThread(Product pro) { this.pro = pro; } @Override public void run() { for (int i = 1; i <= 10; i++) { pro.getProduct(); } }}
功能分解四:測試類
public class Test { public static void main(String[] args) { Product p = new Product(); ProducterThread pth = new ProducterThread(p); CustomerThread cth = new CustomerThread(p); pth.start(); cth.start(); }}
結果:生產者生產一件商品,消費者消費一件商品,交替進行
推薦學習:《》
以上就是詳細瞭解java多執行緒機制的詳細內容,更多請關注TW511.COM其它相關文章!