Java基礎系列:多線程基礎

小夥伴們,咱們認識一下。java

俗世遊子:專一技術研究的程序猿面試

這節咱們來聊一下Java中多線程的東西編程

本人掐指一算:面試必問的點,:slightly_smiling_face:windows

好的,下面在聊以前,咱們先了解一下多線程的基本概念設計模式

基本概念

進程

那咱們先來聊一聊什麼是程序安全

  • 程序是一個指令的集合,和編程語言無關
  • 在CPU層面,經過編程語言所寫的程序最終會編譯成對應的指令集執行

通俗一點來講,咱們在使用的任意一種軟件均可以稱之爲程度,好比:微信

  • QQ,微信,迅雷等等

而操做系統用來分配系統資源的基本單元叫作進程,相同程序能夠存在多個進程多線程

windows系統的話能夠經過任務管理器來進行查看正在執行的進程:編程語言

任務管理器查看進程

進程是一個靜態的概念,在進程執行過程當中,會佔用特定的地址空間,好比:CPU,內存,磁盤等等。能夠說進程是申請系統資源最小的單位且都是獨立的存在ide

並且咱們要注意一點就是:

  • 在單位時間內,進程在一個處理器中是單一執行的,CPU處理器每次只可以處理一個進程。只不過CPU的切換速度特別快

如今CPU所說的4核8線程、6核12線程就是在提升計算機的執行能力

那麼這樣就牽扯到一個問題:上下文切換

當操做系統決定要把控制權從當前進程轉移到某個新進程時, 就會進行上下文切換,即保存當前進程的上下文、恢復新進程的上下文,而後將控制權傳遞到新進程。新進程就會從它上次中止的地方開始

摘自:《深刻理解計算機系統》:1.7.1 進程

這也就是進程數據保存和恢復

線程

好,上面聊了那麼多,終於進入到了主題:線程

前面說進程是申請資源最小的單位,那麼線程是進程中的最小執行單元,是進程中單一的連續控制流程,而且進程中最少擁有一個線程:也就是咱們所所的主線程

若是瞭解過Android開發的話,那麼應該更能明白這一點

進程中最少執行線程名稱

進程中能夠擁有多個並行線程,最少會擁有一個線程。線程在進程中是互相獨立的,多個線程之間的執行不會產生影響,可是若是多個線程操做同一份數據,那麼確定會產生影響(這也就是咱們在前面所說的線程安全問題)

典型案例:賣票

進程中的線程共享相同的內存單元(內存地址空間),包括能夠訪問相同的變量和對象,能夠從同一個堆中分配對象,能夠作通訊,數據交換、數據同步的操做

並且共享進程中的CPU資源,也就是說線程執行順序經過搶佔進程內CPU資源,誰能搶佔上誰就能夠執行。

後面聊到線程狀態再細說

還有一種叫作:纖程/協程(同樣的概念)

更輕量級別的線程,運行在線程內部,是用戶空間級別的線程。後面再聊

面試高頻:進程和線程區別

  1. 最根本的區別:進程是操做系統用來分配資源的基本單位,而線程是執行調度的最小單元
  2. 線程的執行依託於進程,且線程共享進程中的資源

  3. 每一個進程都有獨立的資源空間,CPU在進行進程切換的時候開銷較大,而線程的開銷較小

實現方式

瞭解完了基本概念以後,就要進入到具體的實操環節,在Java中,若是想要建立多線程的話,其表現形式一共有5中方式,記住:是表現形式。

下面咱們先來看其中兩種形式

繼承Thread實現

在Thread源碼中,包含對Java中線程的介紹,如何建立線程的兩種表現形式,包括如何啓動建立好的線程:

Thread中的介紹信息

因此說,一個類的註釋文檔地方很是重要

那麼咱們來本身建立一個線程:

class CusThread1 extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.println("當前執行的線程名稱:" + Thread.currentThread().getName());
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        System.out.println("當前執行線程名稱:" + Thread.currentThread().getName());

        CusThread1 cusThread1 = new CusThread1();
        cusThread1.start();
    }
}

這就是一個最簡單的線程建立,咱們來看一下是不是成功的

Thread第一個程序執行結果

因此說這裏建立線程分爲兩步:

  • 定義一個類,繼承Thread主類並重寫其中的run()
  • 調用start()方法開始執行

這裏須要注意的一點,咱們若是要啓動一個線程的話,必須是調用start()方法,而不能直接調用run(),二者是有區別的:

  • 調用start()方法是Java虛擬機將調用此線程的run()方法,這裏會建立兩個線程:
    • 當前線程(從調用返回到start方法)
    • 執行run()的線程
public synchronized void start() {
    /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}

// 這裏是start()方法中具體開始執行的方法
private native void start0();
  • 而若是直接調用run()方法的話,至關因而普通方法的調用,是不會建立新的線程的,這裏咱們須要重點注意

這是一種方式,可是咱們並不推薦該方式:

  • Java是單繼承的,若是經過繼承Thread,那麼該類還須要繼承其餘類的話,就沒有辦法了
  • Thread啓動時須要new當前對象,若是該類中存在共享屬性的話,那麼就意味着每次建立新的對象都會在新對象的堆空間中擁有該屬性,那麼咱們每次操做該屬性其實操做的就是當前對象堆空間中的屬性

可能會有點難理解,咱們來作個試驗

public class ThreadDemo1 {

    public static void main(String[] args) {
        System.out.println("當前執行線程名稱:" + Thread.currentThread().getName());

        CusThread1 cusThread1 = new CusThread1();
        CusThread1 cusThread2 = new CusThread1();
        CusThread1 cusThread3 = new CusThread1();
        cusThread1.start();
        cusThread2.start();
        cusThread3.start();
    }
}

class CusThread1 extends Thread {

    public int i = 1;

    @Override
    public void run() {
        for (int j = 0; j < 5; j++) {
            System.out.printf("當前線程:%s, i=%s \n", Thread.currentThread().getName(), i++);
        }
    }
}

Thread共享變量

固然,這種問題也是有解決的:

  • 就是將共享變量設置成static,咱們看一下效果

Thread共享變量

實現Runnable接口

那咱們來看下這種方式,Runnable是一個接口,其中只包含run()方法,咱們經過重寫其接口方法就能夠實現多線程的建立

具體實現方式以下

class CusThread2 implements Runnable {
    public int i = 1;

    @Override
    public void run() {
        for (int j = 0; j < 5; j++) {
            System.out.printf("當前線程:%s, i=%s \n", Thread.currentThread().getName(), i++);
        }
    }
}

CusThread2 thread = new CusThread2();

new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();

這裏建立線程並啓動也分爲兩步:

  • 線程類實現Runnable接口,而且重寫run()方法
  • 經過new Thread(Runnable)的形式建立線程並調用start()啓動

這裏推薦採用這種方式,由於:

  • Java雖然是單繼承,可是是多實現的方式,經過Runnable接口的這種方式即不影響線程類的繼承,也能夠實現多個接口
  • 就是共享變量問題,上面看到,線程類中的共享變量沒有定義static,可是不會出現Thread方式中的問題

Runnable共享變量

由於在建立線程的時候,線程類只建立了一次,啓動都是經過Thread類來啓動的,因此就不會出現上面的問題

擴展:代理模式

從這種方式能夠引出一種模式叫作:代理模式。那什麼是代理模式呢?

  • 就是說爲其餘對象提供一種代理對象,經過代理對象來控制這個對象的訪問

好比上面的Runnable/Thread,實際的業務邏輯寫在Runnable接口中,可是咱們倒是經過Thread來控制其行爲如:start, stop等

代理模式的關鍵點在於:

  • 利用了Java特性之一的多態,肯定代理類和被代理類
  • 代理類和被代理類都須要實現同一個接口

這裏給你們推薦一本設計模式的書:《設計模式之禪》

下面咱們來作個案例,深刻了解一下多線程

多窗口賣票案例

下面咱們分別用兩種建立線程的方式來作一下賣票這個小例子:

public class TicketThreadDemo {

    public static void main(String[] args) {

//        startTicketThread();
        startTicketRunnable();

    }

    private static void startTicketRunnable() {
        TicketRunnable ticketRunnable = new TicketRunnable();

        List<Thread> ticketThreads = new ArrayList<Thread>(5) {{
            for (int i = 0; i < 5; i++) {
                add(new Thread(ticketRunnable));
            }
        }};

        ticketThreads.forEach(Thread::start);
    }

    private static void startTicketThread() {
        List<TicketThread> ticketThreads = new ArrayList<TicketThread>(5) {{
            for (int i = 0; i < 5; i++) {
                add(new TicketThread());
            }
        }};

        ticketThreads.forEach(TicketThread::start);
    }
}

// Runnable方式
class TicketRunnable implements Runnable {

    private int ticketCount = 10;

    @Override
    public void run() {
        while (ticketCount > 0) {
            System.out.printf("窗口:%s, 賣出票:%s \n", Thread.currentThread().getName(), ticketCount--);
        }
    }
}

// Thread方式
class TicketThread extends Thread {

    // 記住,共享變量這裏必須使用static,
    private static int ticketCount = 10;

    @Override
    public void run() {
        while (ticketCount > 0) {
            System.out.printf("窗口:%s, 賣出票:%s \n", Thread.currentThread().getName(), ticketCount--);
        }
    }
}

寫到一塊兒,就不拆分了,你們能夠本身嘗試下

TicketDemo

經常使用API屬性及方法

這裏咱們來介紹一下在多線程中經常使用到的一些方法,上面咱們已經使用到了:

  • start()

該方法也介紹過了,這裏就不過多寫了,下面看其餘方法

sleep()

根據系統計時器和調度程序的精度和準確性,使當前正在執行的線程進入休眠狀態(暫時中止執行)達指定的毫秒數。 該線程不會失去任何監視器的全部權

通俗一點介紹,就是將程序睡眠指定的時間,等睡眠時間事後,纔會繼續執行,這是一個靜態方法,直接調用便可。

須要注意的一點:睡眠時間單位是毫秒

// 方便時間字符串的方法,本身封裝的,忽略
System.out.println(LocalDateUtils.nowTimeStr());
try {
    // 睡眠2s
    Thread.sleep(2000L);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println(LocalDateUtils.nowTimeStr());

sleep

isAlive()

驗證當前線程是否活動,活動爲true, 不然爲false

private static void alive() {
    // 上一個例子,我拿來使用一下
    TicketThread ticketThread = new TicketThread();
    System.out.println(ticketThread.isAlive()); // false
    ticketThread.start();
    System.out.println(ticketThread.isAlive()); // true
}

join()

上面咱們知道了線程是經過搶佔CPU資源來執行的,那麼線程的執行確定是不可預測的,可是經過join()方法,會讓其餘線程進入阻塞狀態,等當前線程執行完成以後,再繼續執行其餘線程

public static class JoinThread extends Thread{
    private int i = 5;

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

    @Override
    public void run() {
        while (i > 0) {
            System.out.println("當前線程【" + this.getName() + "】, 執行值【" + i-- + "】");
        }
    }
}

private static void join() {
    JoinThread t1 = new JoinThread("T1");
    JoinThread t2 = new JoinThread("T2");

    // 默認狀況
    t1.start();
    t2.start();

    // 添加了join後的狀況
    t1.start();
    t1.join();

    t2.start();
    t2.join();
}

join

yield

當前線程願意放棄對處理器的當前使用,也就是說當前正在運行的線程會放棄CPU的資源從運行狀態直接進入就緒狀態,而後讓CPU肯定進入運行的線程,若是沒有其餘線程執行,那麼當前線程就會當即執行

當前線程會進入到就緒狀態,等待CPU資源的搶佔

多數狀況下用在兩個線程交替執行

stop

stop()很好理解,強行中止當前線程,不過當前方法由於中止的太暴力已經被JDK標註爲過期,推薦採用另外一個方法:interrupt()

中斷此線程

多線程的狀態

線程主要分爲5種狀態:

  • 新生狀態

就是說線程在剛建立出來的狀態,什麼事情都沒有作

TicketThread ticketThread = new TicketThread();
  • 就緒狀態

當建立出來的線程調用start()方法以後進入到就緒狀態,這裏咱們要注意一點,start()以後並不必定就開始運行,而是會將線程添加到就緒隊列中,而後他們開始搶佔CPU資源,誰能搶佔到誰就開始執行

ticketThread.start();
  • 運行狀態

進入就緒狀態的線程搶佔到CPU資源後開始執行,這個執行過程就是運行狀態。

在這個過程當中業務邏輯開始執行

  • 阻塞狀態

當程序運行過程當中,發生某些異常信息時致使程序沒法繼續正常執行下去,此時會進入阻塞狀態

當進入阻塞狀態的緣由消除後,線程就會從新進入就緒狀態,隨機搶佔CPU資源而後等待執行

形成線程進入阻塞狀態的方法:

  1. sleep()
  2. join()
  • 死亡狀態

當程序業務邏輯正常運行完成或由於某些狀況致使程序結束,這樣就會進入死亡狀態

進入死亡狀態的方法:

  1. 程序正常運行完成
  2. 拋出異常致使程序結束
  3. 人爲中斷

Thread狀態

總結

這篇大部分都是概念,代碼方面不多,你們須要理解一下

就先寫到這裏,還有線程同步,線程池的內容,咱們下一篇繼續介紹

相關文章
相關標籤/搜索