Java基礎系列:多線程基礎

小夥伴們,我們認識一下。

俗世遊子:專注技術研究的程序猿

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

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

好的,下面在聊之前,我們先了解一下多線程的基本概念

基本概念

進程

那我們先來聊一聊什麼是程序

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

通俗一點來說,我們在使用的任意一種軟件都可以稱之爲程度,比如:

  • QQ,微信,迅雷等等

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

windows系統的話可以通過任務管理器來進行查看正在執行的進程:

任務管理器查看進程

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

而且我們要注意一點就是:

  • 在單位時間內,進程在一個處理器中是單一執行的,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狀態

總結

這篇大部分都是概念,代碼方面很少,大家需要理解一下

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章