引言
隨着計算機的配置越來越高,我們需要將進程進一步優化,細分為線程,充分提高圖形化界面的多線程的開發。這就要求對線程的掌握很徹底。
那么話不多說,今天本帥將記錄自己線程的學習。
程序,進程,線程的基本概念+並行與並發:
程序:是為完成特定任務,用某種語言編寫的一組指令的集合,即指一段靜態的代碼,靜態對象。
進程:是程序的一次執行過程,或是正在運行的一個程序,是一個動態的過程,有它自身的產生,存在和消亡的過程。-------生命周期
線程:進程可進一步細化為線程,是一個程序內部的一條執行路徑
即:線程《線程(一個程序可以有多個線程)
程序:靜態的代碼 進程:動態執行的程序
線程:進程中要同時干幾件事時,每一件事的執行路徑成為線程。
並行:多個CPU同時執行多個任務,比如:多個人同時做不同的事
並發:一個CPU(采用時間片)同時執行多個任務,比如秒殺平台,多個人做同件事
線程的相關API
//獲取當前線程的名字
Thread.currentThread().getName()
1.start():1.啟動當前線程2.調用線程中的run方法
2.run():通常需要重寫Thread類中的此方法,將創建的線程要執行的操作聲明在此方法中
3.currentThread():靜態方法,返回執行當前代碼的線程
4.getName():獲取當前線程的名字
5.setName():設置當前線程的名字
6.yield():主動釋放當前線程的執行權
7.join():在線程中插入執行另一個線程,該線程被阻塞,直到插入執行的線程完全執行完畢以后,該線程才繼續執行下去
8.stop():過時方法。當執行此方法時,強制結束當前線程。
9.sleep(long millitime):線程休眠一段時間
10.isAlive():判斷當前線程是否存活
判斷是否是多線程
一條線程即為一條執行路徑,即當能用一條路徑畫出來時即為一個線程
例:如下看似既執行了方法一,又執行了方法2,但是其實質就是主線程在執行方法2和方法1這一條路徑,所以就是一個線程
public class Sample{ public void method1(String str){ System.out.println(str); } public void method2(String str){ method1(str); } public static void main(String[] args){ Sample s = new Sample(); s.method2("hello"); } }
線程的調度
調度策略:
時間片:線程的調度采用時間片輪轉的方式
搶占式:高優先級的線程搶占CPU
Java的調度方法:
1.對於同優先級的線程組成先進先出隊列(先到先服務),使用時間片策略
2.對高優先級,使用優先調度的搶占式策略
線程的優先級
等級:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
方法:
getPriority():返回線程優先級
setPriority(int newPriority):改變線程的優先級
注意!:高優先級的線程要搶占低優先級的線程的cpu的執行權。但是僅是從概率上來說的,高優先級的線程更有可能被執行。並不意味着只有高優先級的線程執行完以后,低優先級的線程才執行。
多線程的創建方式
1. 方式1:繼承於Thread類
1.創建一個集成於Thread類的子類 (通過ctrl+o(override)輸入run查找run方法)
2.重寫Thread類的run()方法
3.創建Thread子類的對象
4.通過此對象調用start()方法
start與run方法的區別:
start方法的作用:1.啟動當前線程 2.調用當前線程的重寫的run方法(在主線程中生成子線程,有兩條線程)
調用start方法以后,一條路徑代表一個線程,同時執行兩線程時,因為時間片的輪換,所以執行過程隨機分配,且一個線程對象只能調用一次start方法。
run方法的作用:在主線程中調用以后,直接在主線程一條線程中執行了該線程中run的方法。(調用線程中的run方法,只調用run方法,並不新開線程)
總結:我們不能通過run方法來新開一個線程,只能調用線程中重寫的run方法(可以在線程中不斷的調用run方法,但是不能開啟子線程,即不能同時干幾件事),start是開啟線程,再調用方法(即默認開啟一次線程,調用一次run方法,可以同時執行幾件事)
多線程例子(火車站多窗口賣票問題)
package com.example.paoduantui.Thread;
import android.view.Window; /** * * 創建三個窗口賣票,總票數為100張,使用繼承自Thread方式 * 用靜態變量保證三個線程的數據獨一份 * * 存在線程的安全問題,有待解決 * * */ public class ThreadDemo extends Thread{ public static void main(String[] args){ window t1 = new window(); window t2 = new window(); window t3 = new window(); t1.setName("售票口1"); t2.setName("售票口2"); t3.setName("售票口3"); t1.start(); t2.start(); t3.start(); } } class window extends Thread{ private static int ticket = 100; //將其加載在類的靜態區,所有線程共享該靜態變量 @Override public void run() { while(true){ if(ticket>0){ // try { // sleep(100); // } catch (InterruptedException e) { // e.printStackTrace(); // } System.out.println(getName()+"當前售出第"+ticket+"張票"); ticket--; }else{ break; } } } }
2. 方式2:實現Runable接口方式
1.創建一個實現了Runable接口的類
2.實現類去實現Runnable中的抽象方法:run()
3.創建實現類的對象
4.將此對象作為參數傳遞到Thread類中的構造器中,創建Thread類的對象
5.通過Thread類的對象調用start()
具體操作,將一個類實現Runable接口,(插上接口一端)。
另外一端,通過實現類的對象與線程對象通過此Runable接口插上接口實現
package com.example.paoduantui.Thread;
public class ThreadDemo01 { public static void main(String[] args){ window1 w = new window1(); //雖然有三個線程,但是只有一個窗口類實現的Runnable方法,由於三個線程共用一個window對象,所以自動共用100張票 Thread t1=new Thread(w); Thread t2=new Thread(w); Thread t3=new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class window1 implements Runnable{ private int ticket = 100; @Override public void run() { while(true){ if(ticket>0){ // try { // sleep(100); // } catch (InterruptedException e) { // e.printStackTrace(); // } System.out.println(Thread.currentThread().getName()+"當前售出第"+ticket+"張票"); ticket--; }else{ break; } } } }
比較創建線程的兩種方式:
開發中,優先選擇實現Runable接口的方式
原因1:實現的方式沒有類的單繼承性的局限性
2:實現的方式更適合用來處理多個線程有共享數據的情況
聯系:Thread也是實現自Runable,兩種方式都需要重寫run()方法,將線程要執行的邏輯聲明在run中
3.新增的兩種創建多線程方式
1.實現callable接口方式:
與使用runnable方式相比,callable功能更強大些:
runnable重寫的run方法不如callaalbe的call方法強大,call方法可以有返回值
方法可以拋出異常
支持泛型的返回值
需要借助FutureTask類,比如獲取返回結果
package com.example.paoduantui.Thread;
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * 創建線程的方式三:實現callable接口。---JDK 5.0新增 *是否多線程?否,就一個線程 * * 比runable多一個FutureTask類,用來接收call方法的返回值。 * 適用於需要從線程中接收返回值的形式 * * //callable實現新建線程的步驟: * 1.創建一個實現callable的實現類 * 2.實現call方法,將此線程需要執行的操作聲明在call()中 * 3.創建callable實現類的對象 * 4.將callable接口實現類的對象作為傳遞到FutureTask的構造器中,創建FutureTask的對象 * 5.將FutureTask的對象作為參數傳遞到Thread類的構造器中,創建Thread對象,並調用start方法啟動(通過FutureTask的對象調用方法get獲取線程中的call的返回值) * * */ //實現callable接口的call方法 class NumThread implements Callable{ private int sum=0;// //可以拋出異常 @Override public Object call() throws Exception { for(int i = 0;i<=100;i++){ if(i % 2 == 0){ System.out.println(Thread.currentThread().getName()+":"+i); sum += i; } } return sum; } } public class ThreadNew { public static void main(String[] args){ //new一個實現callable接口的對象 NumThread numThread = new NumThread(); //通過futureTask對象的get方法來接收futureTask的值 FutureTask futureTask = new FutureTask(numThread); Thread t1 = new Thread(futureTask); t1.setName("線程1"); t1.start(); try { //get返回值即為FutureTask構造器參數callable實現類重寫的call的返回值 Object sum = futureTask.get(); System.out.println(Thread.currentThread().getName()+":"+sum); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }
使用線程池的方式:
背景:經常創建和銷毀,使用量特別大的資源,比如並發情況下的線程,對性能影響很大。
思路:提前創建好多個線程,放入線程池之,使用時直接獲取,使用完放回池中。可以避免頻繁創建銷毀,實現重復利用。類似生活中的公共交通工具。(數據庫連接池)
好處:提高響應速度(減少了創建新線程的時間)
降低資源消耗(重復利用線程池中線程,不需要每次都創建)
便於線程管理
corePoolSize:核心池的大小
maximumPoolSize:最大線程數
keepAliveTime:線程沒有任務時最多保持多長時間后會終止
。。。。。。
JDK 5.0 起提供了線程池相關API:ExecutorService 和 Executors
ExecutorService:真正的線程池接口。常見子類ThreadPoolExecutor.
void execute(Runnable coommand):執行任務/命令,沒有返回值,一般用來執行Runnable
Futuresubmit(Callable task):執行任務,有返回值,一般又來執行Callable
void shutdown():關閉連接池。
Executors | 工具類,線程池的工廠類,用於創建並返回不同類型的線程池 |
---|---|
Executors.newCachedThreadPool() | 創建一個可根據需要創建新線程的線程池 |
Executors.newFixedThreadPool(n) | 創建一個可重用固定線程數的線程池 |
Executors.newSingleThreadExecutor() | :創建一個只有一個線程的線程池 |
Executors.newScheduledThreadPool(n) | 創建一個線程池,它可安排在給定延遲后運行命令或者定期地執行。 |
線程池構造批量線程代碼如下:
package com.example.paoduantui.Thread;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 創建線程的方式四:使用線程池(批量使用線程) *1.需要創建實現runnable或者callable接口方式的對象 * 2.創建executorservice線程池 * 3.將創建好的實現了runnable接口類的對象放入executorService對象的execute方法中執行。 * 4.關閉線程池 * * */ class NumberThread implements Runnable{ @Override public void run() { for(int i = 0;i<=100;i++){ if (i % 2 ==0 ) System.out.println(Thread.currentThread().getName()+":"+i); } } } class NumberThread1 implements Runnable{ @Override public void run() { for(int i = 0;i<100; i++){ if(i%2==1){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } public class ThreadPool { public static void main(String[] args){ //創建固定線程個數為十個的線程池 ExecutorService executorService = Executors.newFixedThreadPool(10); //new一個Runnable接口的對象 NumberThread number = new NumberThread(); NumberThread1 number1 = new NumberThread1(); //執行線程,最多十個 executorService.execute(number1); executorService.execute(number);//適合適用於Runnable //executorService.submit();//適合使用於Callable //關閉線程池 executorService.shutdown(); } }
目前兩種方式要想調用新線程,都需要用到Thread中的start方法。
java virtual machine(JVM):java虛擬機內存結構
程序(一段靜態的代碼)——————》加載到內存中——————》進程(加載到內存中的代碼,動態的程序)
進程可細分為多個線程,一個線程代表一個程序內部的一條執行路徑
每個線程有其獨立的程序計數器(PC,指導着程序向下執行)與運行棧(本地變量等,本地方法等)
線程通信方法:
wait()/ notify()/ notifayAll():此三個方法定義在Object類中的,因為這三個方法需要用到鎖,而鎖是任意對象都能充當的,所以這三個方法定義在Object類中。
由於wait,notify,以及notifyAll都涉及到與鎖相關的操作
wait(在進入鎖住的區域以后阻塞等待,釋放鎖讓別的線程先進來操作)---- Obj.wait 進入Obj這個鎖住的區域的線程把鎖交出來原地等待通知
notify(由於有很多鎖住的區域,所以需要將區域用鎖來標識,也涉及到鎖) ----- Obj.notify 新線程進入Obj這個區域進行操作並喚醒wait的線程
有點類似於我要拉粑粑,我先進了廁所關了門,但是發現廁所有牌子寫着不能用,於是我把廁所鎖給了別人,別人進來拉粑粑還是修廁所不得而知,直到有人通知我廁所好了我再接着用。
所以wait,notify需要使用在有鎖的地方,也就是需要用synchronize關鍵字來標識的區域,即使用在同步代碼塊或者同步方法中,且為了保證wait和notify的區域是同一個鎖住的區域,需要用鎖來標識,也就是鎖要相同的對象來充當
線程的分類:
java中的線程分為兩類:1.守護線程(如垃圾回收線程,異常處理線程),2.用戶線程(如主線程)
若JVM中都是守護線程,當前JVM將退出。(形象理解,唇亡齒寒)
線程的生命周期:
JDK中用Thread.State類定義了線程的幾種狀態,如下:
線程生命周期的階段 | 描述 |
---|---|
新建 | 當一個Thread類或其子類的對象被聲明並創建時,新生的線程對象處於新建狀態 |
就緒 | 處於新建狀態的線程被start后,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源 |
運行 | 當就緒的線程被調度並獲得CPU資源時,便進入運行狀態,run方法定義了線程的操作和功能 |
阻塞 | 在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出CPU並臨時終止自己的執行,進入阻塞狀態 |
死亡 | 線程完成了它的全部工作或線程被提前強制性地中止或出現異常導致結束 |
線程的同步:在同步代碼塊中,只能存在一個線程。
線程的安全問題:
什么是線程安全問題呢?
線程安全問題是指,多個線程對同一個共享數據進行操作時,線程沒來得及更新共享數據,從而導致另外線程沒得到最新的數據,從而產生線程安全問題。
上述例子中:創建三個窗口賣票,總票數為100張票
1.賣票過程中,出現了重票(票被反復的賣出,ticket未被減少時就打印出了)錯票。
2.問題出現的原因:當某個線程操作車票的過程中,尚未完成操作時,其他線程參與進來,也來操作車票。(將此過程的代碼看作一個區域,當有線程進去時,裝鎖,不讓別的線程進去)
生動理解的例子:有一個廁所,有人進去了,但是沒有上鎖,於是別人不知道你進去了,別人也進去了對廁所也使用造成錯誤。
3.如何解決:當一個線程在操作ticket時,其他線程不能參與進來,直到此線程的生命周期結束
4.在java中,我們通過同步機制,來解決線程的安全問題。
方式一:同步代碼塊
使用同步監視器(鎖)
Synchronized(同步監視器){
//需要被同步的代碼
}
說明:
- 操作共享數據的代碼(所有線程共享的數據的操作的代碼)(視作衛生間區域(所有人共享的廁所)),即為需要共享的代碼(同步代碼塊,在同步代碼塊中,相當於是一個單線程,效率低)
- 共享數據:多個線程共同操作的數據,比如公共廁所就類比共享數據
- 同步監視器(俗稱:鎖):任何一個的對象都可以充當鎖。(但是為了可讀性一般設置英文成lock)當鎖住以后只能有一個線程能進去(要求:多個線程必須要共用同一把鎖,比如火車上的廁所,同一個標志表示有人)
Runable天生共享鎖,而Thread中需要用static對象或者this關鍵字或者當前類(window。class)來充當唯一鎖
方式二:同步方法
使用同步方法,對方法進行synchronized關鍵字修飾
將同步代碼塊提取出來成為一個方法,用synchronized關鍵字修飾此方法。
對於runnable接口實現多線程,只需要將同步方法用synchronized修飾
而對於繼承自Thread方式,需要將同步方法用static和synchronized修飾,因為對象不唯一(鎖不唯一)
總結:1.同步方法仍然涉及到同步監視器,只是不需要我們顯示的聲明。
2.非靜態的同步方法,同步監視器是this
靜態的同步方法,同步監視器是當前類本身。繼承自Thread。class
方式三:JDK5.0新增的lock鎖方法
package com.example.paoduantui.Thread;
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{ private int ticket = 100;//定義一百張票 //1.實例化鎖 private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { //2.調用鎖定方法lock lock.lock(); if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "張票"); ticket--; } else { break; } } } } public class LockTest { public static void main(String[] args){ Window w= new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口1"); t3.setName("窗口1"); t1.start(); t2.start(); t3.start(); } }
總結:Synchronized與lock的異同?
相同:二者都可以解決線程安全問題
不同:synchronized機制在執行完相應的代碼邏輯以后,自動的釋放同步監視器
lock需要手動的啟動同步(lock()),同時結束同步也需要手動的實現(unlock())(同時以為着lock的方式更為靈活)
優先使用順序:
LOCK-》同步代碼塊-》同步方法
判斷線程是否有安全問題,以及如何解決:
1.先判斷是否多線程
2.再判斷是否有共享數據
3.是否並發的對共享數據進行操作
4.選擇上述三種方法解決線程安全問題
例題:
package com.example.paoduantui.Thread;
/*** * 描述:甲乙同時往銀行存錢,存夠3000 * * * */ //賬戶 class Account{ private double balance;//余額 //構造器 public Account(double balance) { this.balance = balance; } //存錢方法 public synchronized void deposit(double amt){ if(amt>0){ balance +=amt; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"存錢成功,余額為:"+balance); } } } //兩個顧客線程 class Customer extends Thread{ private Account acct; public Customer(Account acct){ this.acct = acct; } @Override public void run() { for (int i = 0;i<3;i++){ acct.deposit(1000); } } } //主方法,之中new同一個賬戶,甲乙兩個存錢線程。 public class AccountTest { public static void main(String[] args){ Account acct = new Account(0); Customer c1 = new Customer(acct); Customer c2 = new Customer(acct); c1.setName("甲"); c2.setName("乙"); c1.start(); c2.start(); } }
解決單例模式的懶漢式的線程安全問題:
單例:只能通過靜態方法獲取一個實例,不能通過構造器來構造實例
1.構造器的私有化:
private Bank(){}//可以在構造器中初始化東西
private static Bank instance = null;//初始化靜態實例
public static Bank getInstance(){
if(instance!=null){
instance = new Bank();
}
return instance;
}
假設有多個線程調用此單例,而調用的獲取單例的函數作為操作共享單例的代碼塊並沒有解決線程的安全問題,會導致多個線程都判斷實例是否為空,此時就會導致多個實例的產生,也就是單例模式的線程安全問題。
解決線程安全問題的思路:
- 將獲取單例的方法改寫成同部方法,即加上synchronized關鍵字,此時同步監視器為當前類本身。(當有多個線程並發的獲取實例時,同時只能有一個線程獲取實例),解決了單例模式的線程安全問題。
- 用同步監視器包裹住同步代碼塊的方式。
懶漢式單例模式的模型,例如:生活中的限量版的搶購:
當一群人並發的搶一個限量版的東西的時候,可能同時搶到了幾個人,他們同時進入了房間(同步代碼塊內)
但是只有第一個拿到限量版東西的人才能到手,其余人都不能拿到,所以效率稍高的做法是,當東西被拿走時,我們在門外立一塊牌子,售罄。
這樣就減少了線程的等待。即下面效率稍高的懶漢式寫法:
package com.example.paoduantui.Thread;
public class Bank { //私有化構造器 private Bank(){} //初始化靜態實例化對象 private static Bank instance = null; //獲取單例實例,此種懶漢式單例模式存在線程不安全問題(從並發考慮) public static Bank getInstance(){ if(instance==null){ instance = new Bank(); } return instance; } //同步方法模式的線程安全 public static synchronized Bank getInstance1(){ if(instance==null){ instance = new Bank(); } return instance; } //同步代碼塊模式的線程安全(上鎖) public static Bank getInstance2(){ synchronized (Bank.class){ if(instance==null){ instance = new Bank(); } return instance; } } //效率更高的線程安全的懶漢式單例模式 /** * 由於當高並發調用單例模式的時候,類似於萬人奪寶,只有第一個進入房間的人才能拿到寶物, * 當多個人進入這個房間時,第一個人拿走了寶物,也就另外幾個人需要在同步代碼塊外等候, * 剩下的人只需要看到門口售罄的牌子即已知寶物已經被奪,可以不用進入同步代碼塊內,提高了效率。 * * * */ public static Bank getInstance3(){ if (instance==null){ synchronized (Bank.class){ if(instance==null){ instance = new Bank(); } } } return instance; } }
- 52
- 53
線程的死鎖問題:
線程死鎖的理解:僵持,誰都不放手,一雙筷子,我一只你一只,都等對方放手(死鎖,兩者都進入阻塞,誰都吃不了飯,進行不了下面吃飯的操作)
出現死鎖以后,不會出現提示,只是所有線程都處於阻塞狀態,無法繼續
package com.example.paoduantui.Thread;
/** * 演示線程的死鎖問題 * * */ public class Demo { public static void main(String[] args){ final StringBuffer s1 = new StringBuffer(); final StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { //先拿鎖一,再拿鎖二 synchronized (s1){ s1.append("a"); s2.append("1"); synchronized (s2) { s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); //使用匿名內部類實現runnable接口的方式實現線程的創建 new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); synchronized (s1) { s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); } }
運行結果:
1.先調用上面的線程,再調用下面的線程:
2.出現死鎖:
3.先調用下面的線程,再調用上面的線程。
死鎖的解決辦法:
1.減少同步共享變量
2.采用專門的算法,多個線程之間規定先后執行的順序,規避死鎖問題
3.減少鎖的嵌套。
線程的通信
通信常用方法:
通信方法 | 描述 |
---|---|
wait() | 一旦執行此方法,當前線程就進入阻塞狀態,並釋放同步監視器 |
notify | 一旦執行此方法,就會喚醒被wait的一個線程,如果有多個線程,就喚醒優先級高的線程 |
notifyAll | 一旦執行此方法,就會喚醒所有被wait()的線程 |
使用前提:這三個方法均只能使用在同步代碼塊或者同步方法中。
package com.example.paoduantui.Thread;
/** * 線程通信的例子:使用兩個線程打印1—100,線程1,線程2交替打印 * * 當我們不采取線程之間的通信時,無法達到線程1,2交替打印(cpu的控制權,是自動分配的) * 若想達到線程1,2交替打印,需要: * 1.當線程1獲取鎖以后,進入代碼塊里將number++(數字打印並增加)操作完以后,為了保證下個鎖為線程2所有,需要將線程1阻塞(線程1你等等wait())。(輸出1,number為2) * 2.當線程2獲取鎖以后,此時線程1已經不能進入同步代碼塊中了,所以,為了讓線程1繼續搶占下一把鎖,需要讓線程1的阻塞狀態取消(通知線程1不用等了notify()及notifyAll()),即應該在進入同步代碼塊時取消線程1的阻塞。 * * */ class Number implements Runnable{ private int number = 1;//設置共享數據(線程之間對於共享數據的共享即為通信) //對共享數據進行操作的代碼塊,需要線程安全 @Override public synchronized void run() { while(true){ //使得線程交替等待以及通知交替解等待 notify();//省略了this.notify()關鍵字 if(number<100){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+number); number++; try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } public class CommunicationTest { public static void main(String[] args){ //創建runnable對象 Number number = new Number(); //創建線程,並實現runnable接口 Thread t1 = new Thread(number); Thread t2 = new Thread(number); //給線程設置名字 t1.setName("線程1"); t2.setName("線程2"); //開啟線程 t1.start(); t2.start(); } }
sleep和wait的異同:
相同點:一旦執行方法以后,都會使得當前的進程進入阻塞狀態
不同點:
1.兩個方法聲明的位置不同,Thread類中聲明sleep,Object類中聲明wait。
2.調用的要求不同,sleep可以在任何需要的場景下調用,wait必須使用在同步代碼塊或者同步方法中
3.關於是否釋放同步監視器,如果兩個方法都使用在同步代碼塊或同步方法中,sleep不會釋放,wait會釋放
經典例題:生產者/消費者問題:
生產者(Priductor)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品,店員一次只能持有固定數量的產品(比如20個),如果生產者視圖生產更多的產品,店員會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產:如果店中沒有產品了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。
這里可能出現兩個問題:
生產者比消費者快的時候,消費者會漏掉一些數據沒有收到。
消費者比生產者快時,消費者會去相同的數據。
package com.example.paoduantui.Thread;
/** * 線程通信的應用:生產者/消費者問題 * * 1.是否是多線程問題?是的,有生產者線程和消費者線程(多線程的創建,四種方式) * 2.多線程問題是否存在共享數據? 存在共享數據----產品(同步方法,同步代碼塊,lock鎖) * 3.多線程是否存在線程安全問題? 存在----都對共享數據產品進行了操作。(三種方法) * 4.是否存在線程間的通信,是,如果生產多了到20時,需要通知停止生產(wait)。(線程之間的通信問題,需要wait,notify等) * * */ class Clerk{ private int productCount = 0; //生產產品 public synchronized void produceProduct() { if(productCount<20) { productCount++; System.out.println(Thread.currentThread().getName()+":開始生產第"+productCount+"個產品"); notify(); }else{ //當有20個時,等待wait try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } //消費產品 public synchronized void consumeProduct() { if (productCount>0){ System.out.println(Thread.currentThread().getName()+":開始消費第"+productCount+"個產品"); productCount--; notify(); }else{ //當0個時等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread{//生產者線程 private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { try { sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+";開始生產產品......"); while(true){ clerk.produceProduct(); } } } class Consumer implements Runnable{//消費者線程 private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(Thread.currentThread().getName()+":開始消費產品"); while(true){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumeProduct(); } } } public class ProductTest { public static void main(String[] args){ Clerk clerk = new Clerk(); Producer p1 = new Producer(clerk); p1.setName("生產者1"); Consumer c1 = new Consumer(clerk); Thread t1 = new Thread(c1); t1.setName("消費者1"); p1.start(); t1.start(); } }