「JAVA」線程基礎知識不牢固?別愁,我不只梳理好了,還附帶了案例

Java 線程基礎知識梳理以及線程建立案例分析

程序在沒有流程控制的前提下,代碼都是從上而下逐行依次執行的。基於這樣的機制,若是咱們使用程序來實現邊打遊戲,邊聽音樂的需求時,就會很困難;由於按照執行順序,只能從上往下依次執行;同一時刻,只能執行聽音樂和打遊戲的其中之一。爲了解決這樣的問題,在程序設計中引入了多線程併發。本文中的知識對windows、mac、linux系統都適用,但展現界面和功能名稱上不太同樣;相關的截圖這裏以windows爲例。java

並行和併發

並行併發是兩個很容易混淆的概念,他們在字面上理解起來可能沒有很大的差別,但要放在計算機運行環境中來解釋,二者是有很大區別的:linux

  • 並行:多個事件在同一個時間點同時發生,是真正的同時發生
  • 併發:多個事件在同一時間段內在宏觀上同時發生,而在微觀上是CPU在多個事件上來回切換,但切換的時間很快,並不能被人眼捕獲,所以在一段人類能夠觀察到的時間內,多個事件是同時發生的

並行和併發

操做系統的運行環境中,併發指的就是一段時間內宏觀上多個程序在同時運行;在單CPU的環境中,微觀上每個時刻僅有一個程序被CPU執行(也就是僅有一個程序得到了CPU時間片),CPU是在多個程序之間來回交替執行,也就是給每一個程序的運行時間進行調度,從而實現多個程序的併發運行。隨着計算機硬件的不斷髮展,現現在的計算機通常都是有多個CPU的,在這樣的多個CPU的環境中,本來由單個處理器運行的這些程序就能夠被分配給多個CPU來運行,從而實現真程序的並行運行,不管從宏觀上,仍是微觀上,程序都是同時運行的。這樣,程序的運行效率就會大大提升。程序員

PS:CPU時間片就是CPU分配給每一個程序的運行時間。windows

在買電腦的時候,電腦廠商宣傳的「幾核處理器」,其中「核」表示的是CPU有幾個物理核心,可以並行處理幾個程序的調用。想要知道本身電腦是幾核的,能夠打開「任務管理器」來查看。安全

Windows 任務管理器

也能夠經過計算機屬性、設備管理器來查看。服務器

因此,單核處理器是不能並行運行多個任務的,只能是多個任務在單核處理器中併發運行,咱們把每一個任務用一個線程來表示,多個線程在單個處理器中的併發運行咱們稱之爲線程調度。從宏觀上講,多個線程是並行運行的;從微觀上講,多個線程是串行運行的,也就是一個線程一個線程的運行;若是對這裏的宏觀和微觀不太好理解的話,能夠把宏觀看做是站在人的角度看待程序運行,把微觀看做是站在CPU的角度看待程序運行,這樣就好理解多了。多線程

線程和進程

進程:進程是指一個在內存中運行的應用程序,每一個進程在內存中都有一塊獨立的內存空間。每一個軟件均可以啓動多個進程。併發

windows 進程

線程:線程指的是進程中的一個控制單元,也就是進程中的每一個單元任務,一個進程中能夠有多個線程同時併發運行。ide

多進程指的是操做系統中同時運行的多個程序,多線程指的是同一個進程中同時運行的多個任務。操做系統中運行的每一個任務就是一個進程,進程中的每一個任務就是一個線程;操做系統就是一個多任務系統,它能夠有多個進程,每一個進程又能夠有多個線程。函數

多進程和多線程

線程和進程的區別:

  1. 每一個進程都有獨立的內存空間,也就是進程中的數據存儲空間(堆、棧空間)是獨立的,且每一個進程都至少有一個線程;
  2. 對於線程來講:堆內存空間是共享的,棧內存空間是獨立的;線程消耗的資源比進程要小得多,且線程之間是能夠相互通訊的;
  3. 線程是進程的基本組成單元,故也把線程稱做進程元,或者輕型進程;
  4. 線程的執行是經過CPU調度器來決定的,程序員沒法控制;

線程調度:

計算機單個CPU在任意時刻只能執行一條計算機指令,每一個進程只有得到CPU使用權才能執行相關指令;多線程併發,其實就是運行中各個進程輪流獲取CPU的使用權來分別執行各自的任務;在多進程的環境中,會有多個線程處於等待獲取CPU使用權的狀態中,爲這些等待中的線程分配CPU使用權的操做就成爲線程調度。線程調度分爲搶佔式調度分時調度

  • 搶佔式調度:多個線程在瞬間搶佔CPU資源,誰搶到誰就運行,有更多的隨機性;
  • 分時調度:爲等待中的多個線程平均的分配CPU時間片

Java的多線程中線程調度就是使用搶佔式調度的。

多線程

多線程單線程,就比如多行道和單行道,多行道能夠有多輛車同時行駛經過,而單行道只能是多輛車按順序依次行駛經過;多線程同時有多個線程併發運行,單線程只有單個線程對多個任務按順序依次執行。

若是以下載文件爲例:單線程就是隻有一個文件下載的通道,多線程則是同時有多個下載通道在下載文件。當服務器提供下載服務時,下載程序是共享服務器帶寬的,在優先級相同的狀況下,服務器會對下載中的全部線程平均分配帶寬

多線程文件下載

寬帶帶寬是以位(bit)來計算的,而下載速度是以字節(byte)計算的,1 byte = 8 bit,故1024KB/s表明的是上網寬帶爲1M(1024千位),而下載速度須要用1024KB/s除去8,得出128KB/s

多線程是爲了同步完成多項任務,是爲了提升系統總體的效率,而不能提升程序代碼自身的運行效率。

多線程的優點:多線程做爲一種多任務、高併發的運行機制,有其獨到的優點所在:

  1. 進程之間不能共享內存空間,可是線程之間是能夠的(經過堆內存);
  2. 建立進程時,操做系統須要爲其從新分配系統資源;而建立線程耗費的資源會小不少,在實現多任務併發時,相比較於多進程,多線程的效率會高不少;
  3. Java 語言內置了對多線程的支持,而不只僅是簡單的調用底層操做系統的調度;Java 對多線程的支持也很友好,能大大簡化開發成本;

建立進程

Java 中建立進程可經過兩種方式來實現:

1.經過java.lang.Runtime來實現,示例代碼以下:

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

建立線程

1、經過繼承Thread類建立線程;須要注意的是:只有Thread的子類纔是線程類;

  1. 新建立一個類繼承於java.lang.Thread;
  2. 在新建的Thread子類中重寫Thread類中的run方法,在run方法中編寫線程邏輯;
  3. 建立線程對象,執行線程邏輯;
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);        
        }    
    }
}

2、經過實現Runnable接口建立線程;須要注意,這裏的Runnable實現類並非線程類,因此啓動方式和Thread子類會有所不一樣;

  1. 新建立一個類實現java.lang.Runnable;
  2. 在新建的實現類中重寫Runnable類中的run方法,在run方法中編寫線程邏輯;
  3. 建立Thread對象,傳入Runnable實現類對象,執行線程邏輯;

示例代碼以下:

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);        
        }    
    }
}

3、使用匿名內部類建立線程

使用接口的匿名內部類來建立線程,示例代碼以下:

// 使用接口的匿名內部類
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();         
            }        
        }            
    }
}

固然了,也能夠使用Thread類的匿名內部類建立線程,不過這樣的方式不多使用;示例代碼以下:

// 使用Thread類的匿名內部類 
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();          
            }        
        }            
    }
}

多線程案例

案例需求:六一兒童節,設置了搶氣球比賽節目,共有50個氣球,三個小朋友小紅、小強、小明來搶;請使用多線程技術來實現上述比勝過程。

氣球比賽

1、使用繼承Thread類的方式來實現上述案例;示例代碼以下:

public class ExtendsDemo {
    public static void main(String []args) {
        
        new Student("小紅").start();
        new Student("小強").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 + "號氣球");
                num--;
            }
        }
    }
}

經過查看輸出結果,發現一個問題:每一個小朋友都搶到了50個氣球,這和本來只有50個氣球相矛盾了;不過別急,咱們可使用第二種方式:使用實現接口的方式來實現上述案例 來解決。

2、使用實現接口的方式來實現上述案例;示例代碼以下:

public class ImplementsDemo {
    public static void main(String []args) {
        Balloon balloon = new Balloon();
        new Thread(balloon, "小紅").start();
        new Thread(balloon, "小強").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 + "號氣球");
          num--;
        }
        }
    }
    
}

在該案例中咱們是用了Thread.currentThread()方法,該方法的做用是返回當前正在執行的線程對象的引用,因此當前正在執行的線程對象的名稱就能夠這樣來獲取:String name = Thread.currentThread().getName();

經過查看該案例的打印結果,不難發現:三個小朋友一共搶到了50個氣球,符合了需求中規氣球總共有50個的要求。咱們再來分析主函數中的代碼,發現是由於3個線程共享了一個Balloon對象,該對象中的氣球數量就在50個。

共享Balloon對象的堆內存模型

按照這樣的思路,上述使用繼承Thread類的方式中出現的問題就能夠解決了。接下來就對上述兩種實現多線程的方式進行分析和總結:

使用繼承Thread類的方式:

  1. 使用繼承方式來實現多線程在操做上會更加簡便;好比:能夠經過super.getName()來直接獲取當前線程對象的名稱;
  2. 因爲Java 是單繼承的,因此若是繼承了Thread,該類就不能再有其餘的父類了;
  3. 對於搶氣球案例需求來講,並不能很好的解決問題;

使用實現接口的方式:

  1. 相較於繼承方式,實現方式和線程操做會稍加複雜;好比:獲取當前線程名稱須要經過Thread.currentThread().getName();來獲取;
  2. 因爲是使用實現的方式,Java 是支持多實現的,因此除了Runnable接口以外,還能夠實現其餘的接口,繼承另外的類;
  3. 可以很好的實現案例需求:多個線程共享一個資源;

在下一篇文章中,我會繼續使用上述的案例來分析線程不安全以及相關的解決方法,敬請期待。

完結。老夫雖不正經,但老夫一身的才華

相關文章
相關標籤/搜索