天天看點

Java入門:多線程學習筆記與多線程案例:賣票

介紹:程序是正在運作的程式;

單線程:一個程序有一個執行路徑;

多線程:一個程序有多條執行路徑;

一、實作多線程的方法:

①.繼承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接口實作多線程的好處:

  1. 避免了Java單繼承的局限性;
  2. 适合多個相同程式的代碼去處理同一個資源的情況,把線程和程式代碼、資料有效分離,較好的展現了面向對象設計思想;

二、設定和擷取線程名稱

setName():

getName():

  1. 調用setName方法,設定名稱,

//調方法給線程指派

dt01.setName("飛機");

dt02.setName("高鐵");

  1. 在自己定義的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);
           

線程排程

線程有兩種排程模型

  1. 分時排程模型:所有線程輪流獲得CPU的使用權,平均配置設定每個線程占用CPU的時間片;
  2. 搶占排程模型(Java使用該模式):優先級高的線程先獲得CPU使用權,如果優先級相同則随機選一個;

Thread類中設定和擷取線程優先級的方法:

  1. getPriority():傳回此線程的優先級;
  2. 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虛拟機即将推出;

案例:劉備關羽張飛三結義,劉備為大哥,劉備挂了,關羽和張飛也會結束,可能不是立即,還會執行一點;

需設定劉備為主線程,關羽張飛為守護線程;

  • 線程的生命周期
Java入門:多線程學習筆記與多線程案例:賣票

案例1:賣票

需求:某電影院目前正在上映國産大片,共有100張,電影院有三個視窗賣票,請設計一個程式模拟該電影院賣票;

思路:

  • 定義一個類Move實作runnable接口,設定一個成員變量piao = 100;
  • 重寫run()方法實作賣票:
  1. 判斷票數大于0,就買票,并告知是哪個視窗賣的
  2. 賣了票之後總數要減1
  3. 票沒有了也會有人來問,是以用死循環一直賣
  • 定義賣票測試類,
  1. 建立Thread對象
  2. 将Move對象作為參數傳遞給三個Thread的對象并給出視窗編号
  3. 啟動線程

代碼如下:

實作類:

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. 是否是多線程環境;
  2. 是否有共享資料;
  3. 是否有多條語句操作共享資料;

解決方案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--;
                }
            }
        }
    }
}
           

以下圖示是教程中的代碼執行流程注釋資訊,加深了解

Java入門:多線程學習筆記與多線程案例:賣票

使用同步代碼塊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();
    }
}