「JAVA」線程基礎(chǔ)知識(shí)不牢固?別愁,我不僅梳理好了,還附帶了案例

Java 線程基礎(chǔ)知識(shí)梳理以及線程創(chuàng)建案例分析

程序在沒有流程控制的前提下,代碼都是從上而下逐行依次執(zhí)行的?;谶@樣的機(jī)制,如果我們使用程序來實(shí)現(xiàn)邊打游戲,邊聽音樂的需求時(shí),就會(huì)很困難;因?yàn)榘凑請(qǐng)?zhí)行順序,只能從上往下依次執(zhí)行;同一時(shí)刻,只能執(zhí)行聽音樂和打游戲的其中之一。為了解決這樣的問題,在程序設(shè)計(jì)中引入了多線程并發(fā)。本文中的知識(shí)對(duì)windows、mac、linux系統(tǒng)都適用,但展示界面和功能名稱上不太一樣;相關(guān)的截圖這里以windows為例。

并行和并發(fā)

并行并發(fā)是兩個(gè)很容易混淆的概念,他們?cè)谧置嫔侠斫馄饋砜赡軟]有很大的差異,但要放在計(jì)算機(jī)運(yùn)行環(huán)境中來解釋,兩者是有很大區(qū)別的:

  • 并行:多個(gè)事件在同一個(gè)時(shí)間點(diǎn)同時(shí)發(fā)生,是真正的同時(shí)發(fā)生
  • 并發(fā):多個(gè)事件在同一時(shí)間段內(nèi)在宏觀上同時(shí)發(fā)生,而在微觀上是CPU在多個(gè)事件上來回切換,但切換的時(shí)間很快,并不能被人眼捕獲,因此在一段人類可以觀察到的時(shí)間內(nèi),多個(gè)事件是同時(shí)發(fā)生的
并行和并發(fā)

操作系統(tǒng)的運(yùn)行環(huán)境中,并發(fā)指的就是一段時(shí)間內(nèi)宏觀上多個(gè)程序在同時(shí)運(yùn)行;在單CPU的環(huán)境中,微觀上每一個(gè)時(shí)刻僅有一個(gè)程序被CPU執(zhí)行(也就是僅有一個(gè)程序獲得了CPU時(shí)間片),CPU是在多個(gè)程序之間來回交替執(zhí)行,也就是給每個(gè)程序的運(yùn)行時(shí)間進(jìn)行調(diào)度,從而實(shí)現(xiàn)多個(gè)程序的并發(fā)運(yùn)行。隨著計(jì)算機(jī)硬件的不斷發(fā)展,現(xiàn)如今的計(jì)算機(jī)一般都是有多個(gè)CPU的,在這樣的多個(gè)CPU的環(huán)境中,原本由單個(gè)處理器運(yùn)行的這些程序就可以被分配給多個(gè)CPU來運(yùn)行,從而實(shí)現(xiàn)真程序的并行運(yùn)行,無論從宏觀上,還是微觀上,程序都是同時(shí)運(yùn)行的。這樣,程序的運(yùn)行效率就會(huì)大大提高。

PS:CPU時(shí)間片就是CPU分配給每個(gè)程序的運(yùn)行時(shí)間。

在買電腦的時(shí)候,電腦廠商宣傳的“幾核處理器”,其中“核”表示的是CPU有幾個(gè)物理核心,能夠并行處理幾個(gè)程序的調(diào)用。想要知道自己電腦是幾核的,可以打開“任務(wù)管理器”來查看。

Windows 任務(wù)管理器

也可以通過計(jì)算機(jī)屬性、設(shè)備管理器來查看。

所以,單核處理器是不能并行運(yùn)行多個(gè)任務(wù)的,只能是多個(gè)任務(wù)在單核處理器中并發(fā)運(yùn)行,我們把每個(gè)任務(wù)用一個(gè)線程來表示,多個(gè)線程在單個(gè)處理器中的并發(fā)運(yùn)行我們稱之為線程調(diào)度。從宏觀上講,多個(gè)線程是并行運(yùn)行的;從微觀上講,多個(gè)線程是串行運(yùn)行的,也就是一個(gè)線程一個(gè)線程的運(yùn)行;如果對(duì)這里的宏觀和微觀不太好理解的話,可以把宏觀看作是站在人的角度看待程序運(yùn)行,把微觀看作是站在CPU的角度看待程序運(yùn)行,這樣就好理解多了。

線程和進(jìn)程

進(jìn)程:進(jìn)程是指一個(gè)在內(nèi)存中運(yùn)行的應(yīng)用程序,每個(gè)進(jìn)程在內(nèi)存中都有一塊獨(dú)立的內(nèi)存空間。每個(gè)軟件都可以啟動(dòng)多個(gè)進(jìn)程。

windows 進(jìn)程

線程:線程指的是進(jìn)程中的一個(gè)控制單元,也就是進(jìn)程中的每個(gè)單元任務(wù),一個(gè)進(jìn)程中可以有多個(gè)線程同時(shí)并發(fā)運(yùn)行。

多進(jìn)程指的是操作系統(tǒng)中同時(shí)運(yùn)行的多個(gè)程序,多線程指的是同一個(gè)進(jìn)程中同時(shí)運(yùn)行的多個(gè)任務(wù)。操作系統(tǒng)中運(yùn)行的每個(gè)任務(wù)就是一個(gè)進(jìn)程,進(jìn)程中的每個(gè)任務(wù)就是一個(gè)線程;操作系統(tǒng)就是一個(gè)多任務(wù)系統(tǒng),它可以有多個(gè)進(jìn)程,每個(gè)進(jìn)程又可以有多個(gè)線程。

多進(jìn)程和多線程

線程和進(jìn)程的區(qū)別:

  1. 每個(gè)進(jìn)程都有獨(dú)立的內(nèi)存空間,也就是進(jìn)程中的數(shù)據(jù)存儲(chǔ)空間(堆、??臻g)是獨(dú)立的,且每個(gè)進(jìn)程都至少有一個(gè)線程;
  2. 對(duì)于線程來說:堆內(nèi)存空間是共享的,棧內(nèi)存空間是獨(dú)立的;線程消耗的資源比進(jìn)程要小得多,且線程之間是可以相互通信的;
  3. 線程是進(jìn)程的基本組成單元,故也把線程稱作進(jìn)程元,或者輕型進(jìn)程;
  4. 線程的執(zhí)行是通過CPU調(diào)度器來決定的,程序員無法控制;

線程調(diào)度:

計(jì)算機(jī)單個(gè)CPU在任意時(shí)刻只能執(zhí)行一條計(jì)算機(jī)指令,每個(gè)進(jìn)程只有獲得CPU使用權(quán)才能執(zhí)行相關(guān)指令;多線程并發(fā),其實(shí)就是運(yùn)行中各個(gè)進(jìn)程輪流獲取CPU的使用權(quán)來分別執(zhí)行各自的任務(wù);在多進(jìn)程的環(huán)境中,會(huì)有多個(gè)線程處于等待獲取CPU使用權(quán)的狀態(tài)中,為這些等待中的線程分配CPU使用權(quán)的操作就成為線程調(diào)度。線程調(diào)度分為搶占式調(diào)度分時(shí)調(diào)度。

  • 搶占式調(diào)度:多個(gè)線程在瞬間搶占CPU資源,誰搶到誰就運(yùn)行,有更多的隨機(jī)性;
  • 分時(shí)調(diào)度:為等待中的多個(gè)線程平均的分配CPU時(shí)間片;

Java的多線程中線程調(diào)度就是使用搶占式調(diào)度的。

多線程

多線程單線程,就好比多行道和單行道,多行道可以有多輛車同時(shí)行駛通過,而單行道只能是多輛車按順序依次行駛通過;多線程同時(shí)有多個(gè)線程并發(fā)運(yùn)行,單線程只有單個(gè)線程對(duì)多個(gè)任務(wù)按順序依次執(zhí)行。

如果以下載文件為例:單線程就是只有一個(gè)文件下載的通道,多線程則是同時(shí)有多個(gè)下載通道在下載文件。當(dāng)服務(wù)器提供下載服務(wù)時(shí),下載程序是共享服務(wù)器帶寬的,在優(yōu)先級(jí)相同的情況下,服務(wù)器會(huì)對(duì)下載中的所有線程平均分配帶寬

多線程文件下載

寬帶帶寬是以位(bit)來計(jì)算的,而下載速度是以字節(jié)(byte)計(jì)算的,1 byte = 8 bit,故1024KB/s代表的是上網(wǎng)寬帶為1M(1024千位),而下載速度需要用1024KB/s除去8,得出128KB/s。

多線程是為了同步完成多項(xiàng)任務(wù),是為了提高系統(tǒng)整體的效率,而不能提高程序代碼自身的運(yùn)行效率。

多線程的優(yōu)勢:多線程作為一種多任務(wù)、高并發(fā)的運(yùn)行機(jī)制,有其獨(dú)到的優(yōu)勢所在:

  1. 進(jìn)程之間不能共享內(nèi)存空間,但是線程之間是可以的(通過堆內(nèi)存);
  2. 創(chuàng)建進(jìn)程時(shí),操作系統(tǒng)需要為其重新分配系統(tǒng)資源;而創(chuàng)建線程耗費(fèi)的資源會(huì)小很多,在實(shí)現(xiàn)多任務(wù)并發(fā)時(shí),相比較于多進(jìn)程,多線程的效率會(huì)高很多;
  3. Java 語言內(nèi)置了對(duì)多線程的支持,而不僅僅是簡單的調(diào)用底層操作系統(tǒng)的調(diào)度;Java 對(duì)多線程的支持也很友好,能大大簡化開發(fā)成本;

創(chuàng)建進(jìn)程

Java 中創(chuàng)建進(jìn)程可通過兩種方式來實(shí)現(xiàn):

1.通過java.lang.Runtime來實(shí)現(xiàn),示例代碼如下:

public static void main(String []args) throws IOException {
      // 方式一:通過通過java.lang.Runtime來實(shí)現(xiàn)打開 cmd         
      Runtime runtime = Runtime.getRuntime();
      runtime.exec("cmd");      
}
  1. 通過java.lang.ProcessBuilder 來實(shí)現(xiàn),示例代碼如下:
public static void main(String []args) throws IOException {
        // 方式二:通過通過java.lang.ProcessBuilder來實(shí)現(xiàn)打開 cmd 
        ProcessBuilder pb = new ProcessBuilder("cmd");
        pb.start();     
}

創(chuàng)建線程

一、通過繼承Thread類創(chuàng)建線程;需要注意的是:只有Thread的子類才是線程類;

  1. 新創(chuàng)建一個(gè)類繼承于java.lang.Thread;
  2. 在新建的Thread子類中重寫Thread類中的run方法,在run方法中編寫線程邏輯;
  3. 創(chuàng)建線程對(duì)象,執(zhí)行線程邏輯;
public class ExtendsThreadDemo {    
      public static void main(String []args) {      
            for (int i = 0; i < 50; i++) {          
                System.out.println("主線程" + i);          
                if (i == 13) {              
                    NewThread newThread = new NewThread();
                    newThread.start();          
                }       
            }           
       }
}

// 新線程類
class NewThread extends Thread {        
    @Override   
    public void run() {     
        for (int i = 0; i < 50; i++) {          
            System.out.println("新線程" + i);      
        }   
    }
}

二、通過實(shí)現(xiàn)Runnable接口創(chuàng)建線程;需要注意,這里的Runnable實(shí)現(xiàn)類并不是線程類,所以啟動(dòng)方式和Thread子類會(huì)有所不同;

  1. 新創(chuàng)建一個(gè)類實(shí)現(xiàn)java.lang.Runnable;
  2. 在新建的實(shí)現(xiàn)類中重寫Runnable類中的run方法,在run方法中編寫線程邏輯;
  3. 創(chuàng)建Thread對(duì)象,傳入Runnable實(shí)現(xiàn)類對(duì)象,執(zhí)行線程邏輯;

示例代碼如下:

public class ImplementsRunnableDemo {    
    public static void main(String []args) {        
        for (int i = 0; i < 50; i++) {          
            System.out.println("主線程" + i);          
            if (i == 13) {              
                Runnable runnable = new NewRunnableImpl();              
                Thread thread = new Thread(runnable);               
                thread.start();         
            }       
        }           
    }
}

// 新線程類
class NewRunnableImpl implements Runnable {     
    @Override   
    public void run() {     
        for (int i = 0; i < 50; i++) {          
            System.out.println("新線程" + i);      
        }   
    }
}

三、使用匿名內(nèi)部類創(chuàng)建線程

使用接口的匿名內(nèi)部類來創(chuàng)建線程,示例代碼如下:

// 使用接口的匿名內(nèi)部類
public class AnonymousInnerClassDemo {      
    public static void main(String []args) {        
        for (int i = 0; i < 50; i++) {          
            System.out.println("主線程" + i);          
            if (i == 13) {            
                new Thread(new Runnable() {              
                    @Override              
                    public void run() {               
                        for (int j = 0; j < 50; j++) {                  
                            System.out.println("新線程" + j);                
                        }              
                    }           
                }).start();         
            }        
        }           
    }
}

當(dāng)然了,也可以使用Thread類的匿名內(nèi)部類創(chuàng)建線程,不過這樣的方式很少使用;示例代碼如下:

// 使用Thread類的匿名內(nèi)部類 
public class AnonymousInnerClassDemo {      
    public static void main(String []args) {        
        for (int i = 0; i < 50; i++) {          
            System.out.println("主線程" + i);          
            if (i == 13) {            
                new Thread() {              
                    @Override              
                    public void run() {                
                        for (int j = 0; j < 50; j++) {                  
                            System.out.println("新線程" + j);                
                        }              
                    }            
                }.start();          
            }        
        }           
    }
}

多線程案例

案例需求:六一兒童節(jié),設(shè)置了搶氣球比賽節(jié)目,共有50個(gè)氣球,三個(gè)小朋友小紅、小強(qiáng)、小明來搶;請(qǐng)使用多線程技術(shù)來實(shí)現(xiàn)上述比賽過程。

氣球比賽

一、使用繼承Thread類的方式來實(shí)現(xiàn)上述案例;示例代碼如下:

public class ExtendsDemo {
    public static void main(String []args) {
        
        new Student("小紅").start();
        new Student("小強(qiáng)").start();
        new Student("小明").start();
        
    }
}

class Student extends Thread {
    
    private int num = 50;
    private String name;
    public Student(String name) {
        super(name);
        this.name = name;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            if (num > 0) {
                System.out.println(this.name + "搶到了" + num + "號(hào)氣球");
                num--;
            }
        }
    }
}

通過查看輸出結(jié)果,發(fā)現(xiàn)一個(gè)問題:每個(gè)小朋友都搶到了50個(gè)氣球,這和原本只有50個(gè)氣球相矛盾了;不過別急,我們可以使用第二種方式:使用實(shí)現(xiàn)接口的方式來實(shí)現(xiàn)上述案例 來解決。

二、使用實(shí)現(xiàn)接口的方式來實(shí)現(xiàn)上述案例;示例代碼如下:

public class ImplementsDemo {
    public static void main(String []args) {
        Balloon balloon = new Balloon();
        new Thread(balloon, "小紅").start();
        new Thread(balloon, "小強(qiáng)").start();
        new Thread(balloon, "小明").start();
    }
}

// 氣球
class Balloon implements Runnable {
    
    private int num = 50;
    
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
        if (num > 0) {
          System.out.println(Thread.currentThread().getName() + "搶到了" 
                     + num + "號(hào)氣球");
          num--;
        }
        }
    }
    
}

在該案例中我們是用了Thread.currentThread()方法,該方法的作用是返回當(dāng)前正在執(zhí)行的線程對(duì)象的引用,所以當(dāng)前正在執(zhí)行的線程對(duì)象的名稱就可以這樣來獲?。?code>String name = Thread.currentThread().getName();。

通過查看該案例的打印結(jié)果,不難發(fā)現(xiàn):三個(gè)小朋友一共搶到了50個(gè)氣球,符合了需求中規(guī)氣球總共有50個(gè)的要求。我們?cè)賮矸治鲋骱瘮?shù)中的代碼,發(fā)現(xiàn)是因?yàn)?code>3個(gè)線程共享了一個(gè)Balloon對(duì)象,該對(duì)象中的氣球數(shù)量就在50個(gè)。

共享Balloon對(duì)象的堆內(nèi)存模型

按照這樣的思路,上述使用繼承Thread類的方式中出現(xiàn)的問題就可以解決了。接下來就對(duì)上述兩種實(shí)現(xiàn)多線程的方式進(jìn)行分析和總結(jié):

使用繼承Thread類的方式:

  1. 使用繼承方式來實(shí)現(xiàn)多線程在操作上會(huì)更加簡便;比如:可以通過super.getName()來直接獲取當(dāng)前線程對(duì)象的名稱;
  2. 由于Java 是單繼承的,所以如果繼承了Thread,該類就不能再有其他的父類了;
  3. 對(duì)于搶氣球案例需求來說,并不能很好的解決問題;

使用實(shí)現(xiàn)接口的方式:

  1. 相較于繼承方式,實(shí)現(xiàn)方式和線程操作會(huì)稍加復(fù)雜;比如:獲取當(dāng)前線程名稱需要通過Thread.currentThread().getName();來獲取;
  2. 由于是使用實(shí)現(xiàn)的方式,Java 是支持多實(shí)現(xiàn)的,所以除了Runnable接口之外,還可以實(shí)現(xiàn)其他的接口,繼承另外的類;
  3. 能夠很好的實(shí)現(xiàn)案例需求:多個(gè)線程共享一個(gè)資源;

在下一篇文章中,我會(huì)繼續(xù)使用上述的案例來分析線程不安全以及相關(guān)的解決方法,敬請(qǐng)期待。

完結(jié)。老夫雖不正經(jīng),但老夫一身的才華

?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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