Java中多線程的概述、實(shí)現(xiàn)方式、線程控制、生命周期、多線程程序練習(xí)、安全問題的解決

多線程的概述

  • 進(jìn)程
    • 正在運(yùn)行的程序,是系統(tǒng)進(jìn)行資源分配和調(diào)用的獨(dú)立單位。
    • 每一個(gè)進(jìn)程都有它自己的內(nèi)存空間和系統(tǒng)資源。


說起線程,它又分為單線程和多線程

  • 線程
  • 是進(jìn)程中的單個(gè)順序控制流,是一條執(zhí)行路徑
  • 一個(gè)進(jìn)程如果只有一條執(zhí)行路徑,則稱為單線程程序
  • 一個(gè)進(jìn)程如果有多條執(zhí)行路徑,則稱為多線程程序

多線程的實(shí)現(xiàn)(1)

如何實(shí)現(xiàn)多線程的程序呢?

  • 由于線程是依賴進(jìn)程而存在的,所以我們應(yīng)該先創(chuàng)建一個(gè)進(jìn)程出來。而進(jìn)程是由系統(tǒng)創(chuàng)建的,所以我們應(yīng)該去調(diào)用系統(tǒng)功能創(chuàng)建一個(gè)進(jìn)程。Java是不能直接調(diào)用系統(tǒng)功能的,所以,我們沒有辦法直接實(shí)現(xiàn)多線程程序。但是呢?Java可以去調(diào)用C/C++寫好的程序來實(shí)現(xiàn)多線程程序。由C/C++去調(diào)用系統(tǒng)功能創(chuàng)建進(jìn)程,然后由Java去調(diào)用這樣的東西,然后提供一些類供我們使用。我們就可以實(shí)現(xiàn)多線程程序了。

  • 方式1:繼承Thread類

    • 步驟
      A:自定義類MyThread繼承Thread類。
      B:MyThread類里面重寫run()
      C:創(chuàng)建對(duì)象
      D:啟動(dòng)線程

下面我們就自定義一個(gè)MyThread類繼承Thread類啟動(dòng)線程

public class MyThread extends Thread {

    @Override
    public void run() {
        // 自己寫代碼
        // 一般來說,被線程執(zhí)行的代碼肯定是比較耗時(shí)的。所以我們用循環(huán)改進(jìn)
        for (int x = 0; x < 100; x++) {
            System.out.println(x);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {


        // 創(chuàng)建兩個(gè)線程對(duì)象
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.start();
        my2.start();
    }
}

這樣我們就創(chuàng)建并啟動(dòng)了兩個(gè)線程
start()方法:首先啟動(dòng)了線程,然后再由jvm去調(diào)用該線程的run()方法。
那么,我們?cè)诶^承Thread類之后,為什么要重寫run()方法呢?

  • 因?yàn)椴皇穷愔械乃写a都需要被線程執(zhí)行的。而這個(gè)時(shí)候,為了區(qū)分哪些代碼能夠被線程執(zhí)行,java提供了Thread類中的run()用來包含那些被線程執(zhí)行的代碼。

獲取和設(shè)置線程名稱

  • Thread類的基本獲取和設(shè)置方法
    • public final String getName():獲取線程的名稱。
    • public final void setName(String name):設(shè)置線程的名稱
public class MyThread extends Thread {

    public MyThread() {
    }

    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {
        // 創(chuàng)建線程對(duì)象
        //無參構(gòu)造+setXxx()
         MyThread my1 = new MyThread();
         MyThread my2 = new MyThread();
         //調(diào)用方法設(shè)置名稱
         my1.setName("阿杜");
         my2.setName("杜鵬程");
         my1.start();
         my2.start();

        //帶參構(gòu)造方法給線程起名字
        // MyThread my1 = new MyThread("阿杜");
        // MyThread my2 = new MyThread("杜鵬程");
        // my1.start();
        // my2.start();

        //我們可以使用無參構(gòu)造的方法,也可以使用帶參構(gòu)造的方法
    }
}

但是我們要獲取main方法所在的線程對(duì)象的名稱,該怎么辦呢?
遇到這種情況,Thread類提供了一個(gè)很好玩的方法:
public static Thread currentThread():返回當(dāng)前正在執(zhí)行的線程對(duì)象
System.out.println(Thread.currentThread().getName());
這句話如果在main中執(zhí)行,就會(huì)輸出main。會(huì)返回當(dāng)前執(zhí)行的線程對(duì)象

線程控制

  • public static void sleep(long millis):線程休眠
  • public final void join():線程加入
  • public static void yield():線程禮讓
  • public final void setDaemon(boolean on):后臺(tái)線程
  • public final void stop():中斷線程
  • public void interrupt():中斷線程

public static void sleep(long millis):線程休眠

public class ThreadSleep extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x + ",日期:" + new Date());
            // 睡眠1秒鐘
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadSleepDemo {
    public static void main(String[] args) {
        ThreadSleep ts1 = new ThreadSleep();
        ThreadSleep ts2 = new ThreadSleep();

        ts1.setName("阿杜");
        ts2.setName("杜鵬程");

        ts1.start();
        ts2.start();
    }
}

public final void join():線程加入,等待該線程終止

public class ThreadJoin extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x);
        }
    }
}
public class ThreadJoinDemo {
    public static void main(String[] args) {
        ThreadJoin tj1 = new ThreadJoin();
        ThreadJoin tj2 = new ThreadJoin();
        ThreadJoin tj3 = new ThreadJoin();

        tj1.setName("中秋節(jié)");
        tj2.setName("國慶節(jié)");
        tj3.setName("圣誕節(jié)");

        tj1.start();
        try {
            tj1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        tj2.start();
        tj3.start();
    }
}

運(yùn)行程序,我們發(fā)現(xiàn)名字為中秋節(jié)的線程走完了之后才開始走下面的兩個(gè)線程。
給那個(gè)線程用這個(gè)方法就是等待該線程終止后,再繼續(xù)執(zhí)行接下來的線程。

public static void yield():線程禮讓

public class ThreadYield extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x);
            Thread.yield();
        }
    }
}
public class ThreadYieldDemo {
    public static void main(String[] args) {
        ThreadYield ty1 = new ThreadYield();
        ThreadYield ty2 = new ThreadYield();

        ty1.setName("阿杜");
        ty2.setName("杜鵬程");

        ty1.start();
        ty2.start();
    }
}

這個(gè)方法暫停當(dāng)前正在執(zhí)行的線程對(duì)象,并執(zhí)行其他線程。
讓多個(gè)線程的執(zhí)行更和諧,但是不能靠它保證一人一次。

public final void setDaemon(boolean on):守護(hù)線程

public class ThreadDaemon extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x);
        }
    }
}
public class ThreadDaemonDemo {
    public static void main(String[] args) {
        ThreadDaemon td1 = new ThreadDaemon();
        ThreadDaemon td2 = new ThreadDaemon();

        td1.setName("關(guān)羽");
        td2.setName("張飛");

        // 設(shè)置守護(hù)線程
        td1.setDaemon(true);
        td2.setDaemon(true);

        td1.start();
        td2.start();

        Thread.currentThread().setName("劉備");
        for (int x = 0; x < 5; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }
}

運(yùn)行程序可以看到,當(dāng)劉備執(zhí)行完5次后,張飛和關(guān)于也會(huì)執(zhí)行完,并不會(huì)執(zhí)行100次。
將該線程標(biāo)記為守護(hù)線程或用戶線程。
當(dāng)正在運(yùn)行的線程都是守護(hù)線程時(shí),Java 虛擬機(jī)退出。
該方法必須在啟動(dòng)線程前調(diào)用。

**public final void stop():中斷線程 **
public void interrupt():中斷線程

這兩個(gè)方法都是中斷線程的意思,但是他們還是有區(qū)別的,我們來一起研究一下

public class ThreadStop extends Thread {
    @Override
    public void run() {
        System.out.println("開始執(zhí)行:" + new Date());

        // 我要休息10秒鐘,親,不要打擾我哦
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            // e.printStackTrace();
            System.out.println("線程被終止了");
        }

        System.out.println("結(jié)束執(zhí)行:" + new Date());
    }
}

public class ThreadStopDemo {
    public static void main(String[] args) {
        ThreadStop ts = new ThreadStop();
        ts.start();

        // 你超過三秒不醒過來,我就干死你
        try {
            Thread.sleep(3000);
//           ts.stop();
            ts.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我們分別運(yùn)行stop()方法和interrupt()方法。
我們可以發(fā)現(xiàn)stop()方法執(zhí)行后,該線程就停止了,不再繼續(xù)執(zhí)行了
但是interrupt()方法執(zhí)行后,它會(huì)終止線程的狀態(tài),還會(huì)繼續(xù)執(zhí)行run方法里面的代碼。

線程的生命周期圖

線程的生命周期圖.png

多線程的實(shí)現(xiàn)(2)

  • 方式2:實(shí)現(xiàn)Runnable接口
    • 步驟:
      • A:自定義類MyRunnable實(shí)現(xiàn)Runnable接口
      • B:重寫run()方法
      • C:創(chuàng)建MyRunnable類的對(duì)象
      • D:創(chuàng)建Thread類的對(duì)象,并把C步驟的對(duì)象作為構(gòu)造參數(shù)傳遞
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            // 由于實(shí)現(xiàn)接口的方式就不能直接使用Thread類的方法了,但是可以間接的使用
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }
}
public class MyRunnableDemo {
    public static void main(String[] args) {
        // 創(chuàng)建MyRunnable類的對(duì)象
        MyRunnable my = new MyRunnable();

        // 創(chuàng)建Thread類的對(duì)象,并把C步驟的對(duì)象作為構(gòu)造參數(shù)傳遞

        // Thread(Runnable target, String name)
        Thread t1 = new Thread(my, "阿杜");
        Thread t2 = new Thread(my, "杜鵬程");

        t1.start();
        t2.start();
    }
}

這樣我們就實(shí)現(xiàn)了多線程的第二種啟動(dòng)方式

多線程程序練習(xí)

某電影院目前正在上映賀歲大片,共有100張票,而它有3個(gè)售票窗口售票,請(qǐng)?jiān)O(shè)計(jì)一個(gè)程序模擬該電影院售票。

我們分別用兩種實(shí)現(xiàn)多線程的方法來完成這個(gè)需求
1.繼承Thread類來實(shí)現(xiàn)

public class SellTicket extends Thread {

    // 定義100張票
    private static int tickets = 100;

    @Override
    public void run() {
        // 定義100張票
        // 每個(gè)線程進(jìn)來都會(huì)走這里,這樣的話,每個(gè)線程對(duì)象相當(dāng)于買的是自己的那100張票,這不合理,所以應(yīng)該定義到外面
        // int tickets = 100;

        // 是為了模擬一直有票
        while (true) {
            if (tickets > 0) {
                System.out.println(getName() + "正在出售第" +(tickets--) + "張票");
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        // 創(chuàng)建三個(gè)線程對(duì)象
        SellTicket st1 = new SellTicket();
        SellTicket st2 = new SellTicket();
        SellTicket st3 = new SellTicket();

        // 給線程對(duì)象起名字
        st1.setName("窗口1");
        st2.setName("窗口2");
        st3.setName("窗口3");

        // 啟動(dòng)線程
        st1.start();
        st2.start();
        st3.start();
    }
}

這樣我們就實(shí)現(xiàn)了三個(gè)窗口同時(shí)在出售這100張票的多線程程序

2.實(shí)現(xiàn)Runnable接口的方式實(shí)現(xiàn)

public class SellTicket implements Runnable {
    // 定義100張票
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "張票");
            }
        }
    }
}

public class SellTicketDemo {
    public static void main(String[] args) {
        // 創(chuàng)建資源對(duì)象
        SellTicket st = new SellTicket();

        // 創(chuàng)建三個(gè)線程對(duì)象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        // 啟動(dòng)線程
        t1.start();
        t2.start();
        t3.start();
    }
}

我們這個(gè)電影院售票程序,從表面上看不出什么問題,但是在真實(shí)生活中,售票時(shí)網(wǎng)絡(luò)是不能實(shí)時(shí)傳輸?shù)?,總是存在延遲的情況,所以,在出售一張票以后,需要一點(diǎn)時(shí)間的延遲,所以我們每次賣票延遲100毫秒

while (true) {
            if (tickets > 0) {
                // 為了模擬更真實(shí)的場(chǎng)景,我們稍作休息
                try {
                    Thread.sleep(100); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "張票");
            }
        }
    }

這樣我們模擬真是場(chǎng)景,稍作休息了,可是運(yùn)行程序后,還是會(huì)出現(xiàn)下面兩個(gè)問題。

  • 相同的票出現(xiàn)多次
    - CPU的一次操作必須是原子性的
  • 還出現(xiàn)了負(fù)數(shù)的票
    - 隨機(jī)性和延遲導(dǎo)致的

這里就牽扯到了線程的安全問題,線程安全問題在理想狀態(tài)下,不容易出現(xiàn),但一旦出現(xiàn)對(duì)軟件的影響是非常大的。

多線程安全問題

如何解決多線程安全問題呢?

  • 把多個(gè)語句操作共享數(shù)據(jù)的代碼給鎖起來,讓任意時(shí)刻只能有一個(gè)線程執(zhí)行即可。

解決線程安全問題實(shí)現(xiàn)(1)

  • 同步代碼塊
    • 格式:
      • synchronized(對(duì)象){ 需要同步的代碼; }
    • 同步可以解決安全問題的根本原因就在那個(gè)對(duì)象上。該對(duì)象如同鎖的功能。

我們多上面售票的代碼進(jìn)行改進(jìn)

public class SellTicket implements Runnable {
    // 定義100張票
    private int tickets = 100;
    //創(chuàng)建鎖對(duì)象
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "張票");
                }
            }
        }
    }
}

我們只要運(yùn)用同步代碼塊的格式來解決線程的問題就可以,主要就是這里的對(duì)象,必須使用的是同一個(gè)鎖對(duì)象。
所以我們可以來總結(jié)一下同步的特點(diǎn)

同步的特點(diǎn)

  • 同步的前提
    • 多個(gè)線程
    • 多個(gè)線程使用的是同一個(gè)鎖對(duì)象
  • 同步的好處
    • 同步的出現(xiàn)解決了多線程的安全問題。
  • 同步的弊端
    • 當(dāng)線程相當(dāng)多時(shí),因?yàn)槊總€(gè)線程都會(huì)去判斷同步上的鎖,這是很耗費(fèi)資源的,無形中會(huì)降低程序的運(yùn)行效率。

解決線程安全問題實(shí)現(xiàn)(2)

我們 還有一種方法可以解決多線程的安全問題
同步方法:就是把同步的關(guān)鍵字加到方法上

private synchronized void sellTicket() {
            if (tickets > 0) {
            try {
                    Thread.sleep(100);
            } catch (InterruptedException e) {
                    e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                        + "正在出售第" + (tickets--) + "張票 ");
            }
    }

我們只要調(diào)用這個(gè)方法就可以了
我們也可以讓此方法為靜態(tài)的方法

private static synchronized void sellTicket() {
        if (tickets > 0) {
        try {
                Thread.sleep(100);
        } catch (InterruptedException e) {
                e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()
                    + "正在出售第" + (tickets--) + "張票 ");
        }
    }

我們要來總結(jié)一下,同步代碼塊的鎖對(duì)象可以時(shí)任意對(duì)象。
但是,當(dāng)把同步關(guān)鍵字加在方法上,它的對(duì)象是this
當(dāng)此方法為精態(tài)方法時(shí),它的對(duì)象是類的字節(jié)碼文件對(duì)象,也就是 類名.class

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,728評(píng)論 18 399
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,569評(píng)論 19 139
  • 本文主要講了java中多線程的使用方法、線程同步、線程數(shù)據(jù)傳遞、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等。 首先講...
    李欣陽閱讀 2,599評(píng)論 1 15
  • 交到一個(gè)閨蜜,往往都是從某一方的主 動(dòng)打招呼開始,然后認(rèn)識(shí),然后熟悉,然后 自然而然地成為朋友,然后發(fā)現(xiàn):哎...
    薏蔦閱讀 521評(píng)論 0 0
  • 我下載的是dmg模式的mysql 一直點(diǎn)下一步,直到完成;打開終端,在命令行輸入 mysql --version ...
    OREOs閱讀 363評(píng)論 0 0

友情鏈接更多精彩內(nèi)容