java多線程編程


    所謂的多線程編程本質上是並發編程,並發編程的本質是指發揮出所有硬件的最大性能。

    Java 是為數不多的真正支持有多線程並發編程的開發語言。所以Java 在整體的處理性能上是最高的。

    如果要了解線程的性能問題,那么首先要先解決的就是清楚什么叫做進程?

 

 

 

    從計算機發展的歷史來講,傳統的硬件只有一個 CPU(單核心的CPU),於是為了發揮出硬件的全部性能,引入了多進程的編程模式。


 

 

 

 

      多進程是指在沒有擴展原始系統硬件資源的情況下,利用一些算法,可以實現多個進程的並行執行。在同一個時間段上,會有多個進程並發執行,但是在同一個時間點上只會有一個進程執行。

    線程實在進程基礎上的進一步划分,可以達到更快的處理性能,任何一個進程的啟動速度實際上都是非常緩慢的,所以線程本質上的設計性能上要遠遠高於進程,但是線程不可能離開進程存活。

 

 

    每一個進程的執行都必須有一個獨立執行它的 CPU,所以所有的資源共享里只有 CPU是無法進行共享的,每個進程都有一個自己的中央處理單元。如果要想實現CPU的共享,那么就必須利用線程來描述。

    隨着硬件和軟件的發展,硬件中的CPU出現了多核的狀態,理論上多核CPU的多進程執行稱為並行編程,並非所有的CPU都可以超出若干個線程出來,一般來講,每一塊CPU 只會有一個線程執行,但是有些CPU 可以使用超線程技術,設計出若干個多線程的執行狀態。

     在進程和線程的概念之上實際上還有一個所謂的”纖程“,在線程基礎上的進一步划分,但有些地方稱之為”協程“。Java並沒有支持多協程編程(以后可能有)。現在比較常見的編程語言里:KotlinAndroid 第二代產品)和Python都是支持多協程編程的。

 

 

  所有的Java 程序執行需要通過一個主方法完成,主方法會作為程序的起點。

若要進行多線程編程,也要有一個線程的起點結構。這個結構就稱為線程類,所有的線程類都是有繼承要求的,可以有三種實現方式:

  • 繼承Thread類;
  • 實現Runnable接口;
  • 實現Callable接口;  

1)繼承Thread實現多線程

java.lang.Thread 是由系統定義的一個線程處理類,任何的子類只需要繼承此類,就可以得到一個線程處理的子類,在繼承時一定要覆寫Thread 類中的 run() 方法,那么這個方法成為線程啟動的主方法存在。

 

范例:定義一個線程的主體類:

 

 

 

 

        如果要想正常的進行多線程的並發執行,那么就要調用本機操作系統提供的底層的函數支持,所以多線程的啟動並不是依靠 run()完成的,他需要通過 Start()方法進行啟動。而所有的Start() 啟動后將調用 run()方法中定義的方法體。

public void start() 

 范例:啟動多線程

class MyThread extends Thread{
    private  String name;
    public MyThread(String name){
        this.name =name;
    }
    @Override
    public void run() {
      //覆寫run()方法
    for (int i =0; i<50;i++){
        System.out.println("【"+this.name+"- 線程】運行,i="+i);
          }
    }
}

public class MyBlog2 {
    public static void main(String[] args) {
        MyThread threadA = new MyThread("線程A");
        MyThread threadB = new MyThread("線程B");
        MyThread threadC = new MyThread("線程C");
        threadA.start();  //通過Thread 類繼承而來
        threadB.start();
        threadC.start();
    }
}
運行結果(隨機抽取):
................... 【線程A
- 線程】運行,i=49 【線程C- 線程】運行,i=0 【線程B- 線程】運行,i=0 【線程C- 線程】運行,i=1 【線程B- 線程】運行,i=1
..................

       通過執行的結果,所有的線程對象屬於交替的執行過程,並且都在交替執行run() 方法定義的方法體。進一步的解釋:關於start() 方法?

      首先觀察start() 的實現源代碼:

 public synchronized void start() {
    
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
               
            }
        }
    }

    private native void start0();


    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

        在每一個start()方法里都會拋出 “IllegalThreadStateException” 異常,而此異常是 RuntimeException 的一個子類,用戶可以根據自己的需要選擇性處理,此異常在重復啟動多線程的時候才會拋出。

        此時會發現 start()方法里定義了一個 start0()方法,而start0()方法沒有方法體,但是使用了一個native的關鍵字定義,此關鍵字的作用在於此操作將交由底層實現。

 

 

       在Java開發領域存在一個稱為 JNI(Java Native Iterface)的技術,利用Java 技術調用底層操作系統函數,但是一般JavaEE 中這個開發較少見,因為這樣寫,Java的可移植性就會喪失,在之前的Android 開發里,JNI技術比較常見。

public class MyBlog2 {
    public static void main(String[] args) {
        MyThread threadA = new MyThread("線程A");
        MyThread threadB = new MyThread("線程B");
        MyThread threadC = new MyThread("線程C");
        threadA.start();  //通過Thread 類繼承而來
        threadB.start();
        threadC.start();
        threadC.start(); //讓他重復啟動
    }
}
Exception in thread "main" java.lang.IllegalThreadStateException
    at java.base/java.lang.Thread.start(Thread.java:794)
    at ThreadTest.MyBlog2.main(MyBlog2.java:24)
【線程A- 線程】運行,i=0
【線程C- 線程】運行,i=0
....................................
//省略下面。。

一個線程只允許啟動一次。

 

 

 2)實現 Runnable 接口

    除了使用 Thread 類實現多線程之外,也可以使用java.lang .Runnable 接口來完成,首先觀察一下Runnable 接口。

@FunctionalInterface
public interface Runnable{
public void run();
}

在Runnable 接口中同樣存在有一個 run() 方法,此方法作為線程主方法存在。

使用Runnable實現多線程:

class MyThread implements Runnable{
    private  String name;
    public MyThread(String name){
        this.name =name;
    }
    @Override
    public void run() {
      //覆寫run()方法
    for (int i =0; i<50;i++){
        System.out.println("【"+this.name+"- 線程】運行,i="+i);
          }
    }
}

       在之前繼承了 Thread類實現的多線程,會自動將父類的 start方法繼承而來,但若使用Runnable來實現,該接口並沒有提供 start() 方法,同時關鍵型的問題是:多線程的啟動只能夠依靠Thread 類的 start() 方法。所以此時關注一下Thread類里提供的構造方法:

public Thread(Runnable target) 

      這個構造方法里面需要接受 Runnable 接口對象的實例。那么此時只需要按照標准調用即可。

范例:啟動多線程:

public class MyBlog2 {
    public static void main(String[] args) {
        MyThread threadA = new MyThread("線程A");
        MyThread threadB = new MyThread("線程B");
        MyThread threadC = new MyThread("線程C");
        new  Thread(threadA).start();
        new  Thread(threadB).start();
        new  Thread(threadC).start();
    }
}
//程序執行結果(隨機抽取)
【線程B- 線程】運行,i=0 【線程C- 線程】運行,i=0 【線程C- 線程】運行,i=1 【線程C- 線程】運行,i=2 【線程C- 線程】運行,i=3 //..........省略后面的結果了。。。。。。

       那么此時就實現了與之前完全相同的操作功能,,但是很明顯這樣的實現避免了繼承帶來的單繼承的局限,所以更適合項目的編寫,同時在JDK1.8之后,Runnable成為了一個函數時接口,所以此時代碼也可以使用 Lambda表達式進行定義。

范例:使用Lambda 實現多線程:

public class MyBlog2 {
    public static void main(String[] args) {
      String names[] = new String[]{"線程A","線程B","線程C"};
        for (String name :names) {
                 new Thread(()->{
                     for (int i =0; i<50;i++){
                         System.out.println("【"+name+"- 線程】運行,i="+i);
                     }
                 }).start();
        }
    }
}
運行結果(部分抽取):

【線程A- 線程】運行,i=3
【線程B- 線程】運行,i=26
【線程C- 線程】運行,i=30
【線程B- 線程】運行,i=27
【線程B- 線程】運行,i=28

       從JDK1.8 之后 Lambda表達式的出現實際上可以達到簡化線程類定義的功能。

 

 

  • Thread 類與Runnable 接口的關系

    在JDK 1.0 的時代就提供了 Thread類和 Runnable 接口,所以這兩個實現方案就經常被人拿來比較,我將從兩者實現的關聯及區別進行說明,首先觀察一下 Thread 類的定義結構:

public class Thread extends Object implements Runnable

    可以發現此時的 Thread 類也是 Runnable 接口的子類,所以可以得到如下結構圖:

 

 解釋問題:為什么Thread 接受了 Runnable 接口對象之后會去調用真實線程中的 run() 方法?

1、關注 Thread類中接受 Runnable 接口對象的構造方法:

 public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
this.target = target;
private Runnable target;

      當Runnable 接口傳到 Thread 類中之后,會自動利用Thread類中的target 屬性保存 Runnable 接口實例。

2、觀察 Thread類中的 run() 方法(調用start()就調用Thread 類中的run()方法)

@Override
public void run(){
   if (target != null){
       target.run();
    }
}

       可以發現在 Thread.run()方法定義的時候會判斷是否有 target 實例,如果有實例,則會調用相應的run()方法;

       在兩種多線程實現方法下,實際上Runnable 接口相比較 Thread 而言,可以更加方便的描述數據共享的概念,即多個線程並行操作同一個資源(方法體)。

范例:觀察資源共享:

class Mythread implements Runnable{
    private String name;
    private int ticket = 20;  //共買20張票

    @Override
    public void run() {
        for (int x = 0; x < 50; x++) {
            if (this.ticket >0 ){
                System.out.println("- 賣票:"+ticket--);
            }
        }
    }
}
public class MyBlog1  {
    public static void main(String[] args) {
        Mythread threadBody =new Mythread();//定義多線程的公共處理
        new Thread(threadBody).start();
        new Thread(threadBody).start();
        new Thread(threadBody).start();
    }
}
運行結果(部分抽取):
- 賣票:20
- 賣票:19
- 賣票:18
- 賣票:16
- 賣票:17
- 賣票:14

 

 

class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name =name;
}
@Override
public void run() {
//覆寫run()方法
for (int i =0; i<50;i++){
System.out.println("【"+this.name+"- 線程】運行,i="+i);
}
}
}

public class MyBlog2 {
public static void main(String[] args) {
MyThread threadA = new MyThread("線程A");
MyThread threadB = new MyThread("線程B");
MyThread threadC = new MyThread("線程C");
threadA.start(); //通過Thread 類繼承而來
threadB.start();
threadC.start();
}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM