介紹:程序是正在運作的程式;
單線程:一個程序有一個執行路徑;
多線程:一個程序有多條執行路徑;
一、實作多線程的方法:
①.繼承Thread類;
a.定義類繼承Thread類;
b.在類中重寫run()方法;
c.建立該類的對象;
d.啟動線程;
❤兩個小問題:
A.為什麼重寫run()方法?
因為run()方法是用來封裝被線程執行的代碼
B.run()方法和start()方法的差別?
run():分裝線程執行的代碼,直接調用,相當于普通方法
start():啟動線程,由JVM調用此線程的run()方法1.a.定義類繼承Thread類、b.在類中重寫run()方法;
public class duothread01 extends Thread{
@Override
public void run() {
for(int x=0;x<25;x++){
System.out.println(getName() + ":" + x);
}
}
}
2.c.建立該類的對象、d.啟動線程;
duothread01 dt01 = new duothread01();
duothread01 dt02 = new duothread01();
//啟動線程
dt01.start();
dt02.start();
②.實作runnable接口;
a.定義一個類實作runnable接口;
b.類中重寫run方法;
c.建立對象;
d.建立Thread類的對象,把聲明的類對象作為參數
e.啟動線程;第一步、a.定義一個類實作runnable接口、b.類中重寫run方法;
package runnable01;
public class runnable01 implements Runnable {
//重寫方法
@Override
public void run() {
for(int i= 0;i<20;i++){
//這裡因為與Thread沒有直接關系,是以需要擷取主線程再擷取線程名稱
System.out.println(Thread.currentThread().getName()+ "," + i);
}
}
}
第二步、c.建立對象、d.建立Thread類的對象,把聲明的類對象作為參數、e.啟動線程;
package runnable01;
public class runnableDemo {
public static void main(String[] args) {
//建立實作類對象
runnable01 rb = new runnable01();
//建立線程:建立Thread類的對象,把runnable實作類對象作為參數,設定名稱,不設定有預設值;
Thread t1 = new Thread(rb,"火箭");
Thread t2 = new Thread(rb,"飛船");
//啟動線程
t1.start();
t2.start();
}
}
❤相比繼承Thread類,實作runnable接口實作多線程的好處:
- 避免了Java單繼承的局限性;
- 适合多個相同程式的代碼去處理同一個資源的情況,把線程和程式代碼、資料有效分離,較好的展現了面向對象設計思想;
二、設定和擷取線程名稱
setName():
getName():
- 調用setName方法,設定名稱,
//調方法給線程指派
dt01.setName("飛機");
dt02.setName("高鐵");
- 在自己定義的Thread的子類中添加無參和帶參構造方法,帶參方法内部用super通路父類帶參方法。
**************************Thread類中***************************
public duothread01() {
}
public duothread01(String name) {
super(name);
}
************************測試類中**************************
//通過帶參構造方法
duothread01 dt03 = new duothread01("飛船");
duothread01 dt04 = new duothread01("火箭");
3.擷取目前正在執行的線程的名稱
Thread currentThread():傳回對目前正在執行的線程對象的引用
String name = Thread.currentThread().getName();
System.out.println(name);
線程排程
線程有兩種排程模型
- 分時排程模型:所有線程輪流獲得CPU的使用權,平均配置設定每個線程占用CPU的時間片;
- 搶占排程模型(Java使用該模式):優先級高的線程先獲得CPU使用權,如果優先級相同則随機選一個;
Thread類中設定和擷取線程優先級的方法:
- getPriority():傳回此線程的優先級;
- setPriority():修改此線程的優先級;
//擷取線程優先級
System.out.println(dt1.getPriority());
System.out.println(dt2.getPriority());
System.out.println(dt3.getPriority());
System.out.println(dt4.getPriority());
//擷取優先級參數的取值範圍
System.out.println(Thread.MAX_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);
//修改線程優先級
dt1.setPriority(1);
dt2.setPriority(10);
dt3.setPriority(9);
dt4.setPriority(2);
線程預設優先級是5,取值範圍是1-10;
線程優先級高僅僅表示線程擷取CPU時間片的幾率高,該線程不一定會跑到最前面,可能在運作次數多的時候才能看到你想要的效果;
- 線程控制
Sleep(long millis):使目前正在執行的線程停留(暫停)指定的毫秒數;
案例:劉備孫權曹操争天下,勢均力敵,不能讓一個先跑完;
Join():等待這個線程死亡;如果有一個線程調用了此方法,那麼其他線程必須得等到這個線程執行完才能執行;
案例:康熙、四阿哥、八阿哥搶皇位,需等康熙挂掉,兩位阿哥開始搶;
setDaemon(boolean on):将此線程标記為守護線程,當運作的線程都是守護線程時,Java虛拟機即将推出;
案例:劉備關羽張飛三結義,劉備為大哥,劉備挂了,關羽和張飛也會結束,可能不是立即,還會執行一點;
需設定劉備為主線程,關羽張飛為守護線程;
- 線程的生命周期
案例1:賣票
需求:某電影院目前正在上映國産大片,共有100張,電影院有三個視窗賣票,請設計一個程式模拟該電影院賣票;
思路:
- 定義一個類Move實作runnable接口,設定一個成員變量piao = 100;
- 重寫run()方法實作賣票:
- 判斷票數大于0,就買票,并告知是哪個視窗賣的
- 賣了票之後總數要減1
- 票沒有了也會有人來問,是以用死循環一直賣
- 定義賣票測試類,
- 建立Thread對象
- 将Move對象作為參數傳遞給三個Thread的對象并給出視窗編号
- 啟動線程
代碼如下:
實作類:
package Moveticket_anli;
public class Move_ticket implements Runnable {
//定義變量,總票數指派給它
private int ticket = 20;
//重寫run()方法
@Override
public void run() {
//死循環實作一直賣票,即使賣完了也可以通路;
while (true) {
//判斷票數大于0,就賣票
if (ticket > 0) {
//拼接:視窗名+第幾張
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "張票");
//賣出後總數減1
ticket--;
}
}
}
}
測試類:
package Moveticket_anli;
public class movewin {
public static void main(String[] args) {
//建立賣票類的對象
Move_ticket mt = new Move_ticket();
//建立三個線程(視窗)同時賣票
Thread t1 = new Thread(mt,"wein01");
Thread t2 = new Thread(mt,"wein02");
Thread t3 = new Thread(mt,"wein03");
//啟動線程
t1.start();
t2.start();
t3.start();
}
}
案例思考1:
現實生活中,票賣出去,出片也需要時間,是以在每賣出一張票,需要一點時間的延遲,是以給賣票動作加一個100毫秒的延遲操作,用sleep()方法實作,實作接口類的if内部開頭添加如下代碼:
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
案例思考2:
修改代碼後發現,同一張票會被出售多次,并且最後出現了第0和第-1張票;
出現此問題的原因是線程的随機性導緻的,稱為資料安全問題;
資料安全問題:如果符合下面全部三種情況,即可判定程式存在資料安全問題;
- 是否是多線程環境;
- 是否有共享資料;
- 是否有多條語句操作共享資料;
解決方案1:使用同步代碼塊sychronized(參數)
參數:在外部定義private Object obj = new Object();對象名作為參數;
資料安全問題的存在是因為同時滿足了三個條件,那麼要解決就得至少改變一條使其不滿足,多線程和共享資料是無法避免的,是以隻能是改變多條語句操作共享資料的方案,把多條語句操作共享資料的代碼給鎖起來,讓任意時刻都隻有一個線程能夠執行這段代碼;
Java提供了同步代碼塊的操作:
格式:
sychronized(任意對象){
多條語句操作共享資料的代碼
}
sychronized(任意對象):任意對象就可以看作是一把鎖;
最終代碼如下:
package Moveticket_anli;
public class Move_ticket implements Runnable {
//定義變量,總票數指派給它
private int ticket = 20;
private Object obj = new Object();
//重寫run()方法
@Override
public void run() {
//死循環實作一直賣票,即使賣完了也可以通路;
while (true) {
//把多條語句操作共享資料的代碼給鎖起來
// 讓任意時刻都隻有一個線程能夠執行這段代碼
synchronized (obj) {
//判斷票數大于0,就賣票
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//拼接:視窗名+第幾張
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "張票");
//賣出後總數減1
ticket--;
}
}
}
}
}
以下圖示是教程中的代碼執行流程注釋資訊,加深了解
使用同步代碼塊sychronized(參數)的好處與弊端
好處:解決了多線程的資料安全問題;
弊端:當線程很多時,因為每個線程都會去判斷線程上的鎖,非常耗費資源,無形中降低程式的運作效率;
解決方案2:同步方法
同步方法:将sychronized寫到方法的修飾符後面;
Public sychronized void XXX(){};
鎖對象是:this
同步靜态方法:将sychronized寫到方法的靜态修飾符後面;
Public static sychronized void XXX(){};
鎖對象是:類名.class
用同步方法改進後代碼如下:
賣票方法:
package Moveticket_anli;
public class Move_ticket implements Runnable {
private int ticket = 20;
private Object obj = new Object();
@Override
public void run() {
//死循環實作一直賣票,即使賣完了也可以通路;
while (true) {
maipiao();
}
}
//把多條語句操作共享資料的代碼給鎖起來
// 讓任意時刻都隻有一個線程能夠執行這段代碼
private synchronized void maipiao() {
//判斷票數大于0,就賣票
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//拼接:視窗名+第幾張
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "張票");
//賣出後總數減1
ticket--;
}
}
}
賣票視窗:
package Moveticket_anli;
public class wicket {
public static void main(String[] args) {
//建立賣票類的對象
Move_ticket mt = new Move_ticket();
//建立三個線程(視窗)同時賣票
Thread t1 = new Thread(mt,"windows01");
Thread t2 = new Thread(mt,"windows02");
Thread t3 = new Thread(mt,"windows03");
//啟動線程
t1.start();
t2.start();
t3.start();
}
}