快速認識線程

 本文參考自Java高併發編程詳解

 

一、建立並啓動一個線程

下面是不添加線程的程序代碼。java

package concurrent.chapter01;

import java.util.concurrent.TimeUnit;
public class TryConcurrency {
    public static void main(String[] args) {
        browseNews();
        enjoyMusic();
    }
    private static void browseNews() {
        while(true) {
            System.out.println("Uh-huh,the good news.");
            sleep(1);
        }
    }
    private static void enjoyMusic() {
        while(true) {
            System.out.println("Uh-huh,the nice music");
            sleep(1);
        }
    }
    private static void sleep(int i) {
        try {
            TimeUnit.SECONDS.sleep(i);
        }catch (Exception e) {
            
        }
    }
}

運行結果以下:算法

程序永遠不會執行第二個方法。所以咱們須要使用線程。sql

這裏經過匿名內部類的方式建立線程,而且重寫其中的run方法,使程序交互運行。數據庫

package concurrent.chapter01;

import java.util.concurrent.TimeUnit;
public class TryConcurrency {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                enjoyMusic();
            }
        }.start();
        browseNews();
    }
    private static void browseNews() {
        while(true) {
            System.out.println("Uh-huh,the good news.");
            sleep(1);
        }
    }
    private static void enjoyMusic() {
        while(true) {
            System.out.println("Uh-huh,the nice music");
            sleep(1);
        }
    }
    private static void sleep(int i) {
        try {
            TimeUnit.SECONDS.sleep(i);
        }catch (Exception e) {
            
        }
    }
}

運行結果以下:編程

注意:設計模式

一、建立一個線程,須要重寫Thread中的run方法,Override的註解是重寫的標識,而後將enjoyMusic交給他執行。安全

二、啓動新的線程須要重寫Thread的start方法,才表明派生了一個新的線程,不然Thread和其餘普通的Java對象並沒有區別,start放法是一個當即返回方法,並不會讓程序陷入阻塞。網絡

若是使用Lambda表達式改造上面的代碼,那麼代碼會變得更簡潔。數據結構

public static void main(String[] args) {
        new Thread(TryConcurrency::enjoyMusic).start();
        browseNews();
    }

二、線程的建立與結束生命週期。

一、線程的new狀態。

當咱們用關鍵字new建立一個Thread對象時,此時他並不處於執行狀態,由於沒用start啓動該線程,那麼線程的狀態爲NEW狀態,準確的說,它只是Thread對象的狀態,由於在沒用start以前,該線程根本不存在,與你用new建立一個普通的Java對象沒什麼區別。併發

二、線程的RUNNABLE狀態

線程對象進入RUNNABLE狀態必須調用start方法,那麼此時纔是真正地在JVM中建立了一個線程,線程一經啓動就能夠當即執行嗎?答案是否認的,線程的運行與否和進程同樣都要聽令於CPU的調度,那麼咱們把這個中間狀態成爲可執行狀態,也就是說它具有執行的資格,可是並無真正地執行起來,而是等待CPU的調度。

三、線程的RUNNING狀態

一旦CPU經過輪詢或者其餘方式從任務可執行隊列中選中了線程,那麼此時它才能真正的執行本身的邏輯代碼,須要說明一點是一個正在RUNNING狀態的線程事實上也是RUNNABLE的,可是反過來則不成立。

在該狀態中,線程的狀態能夠發生以下的狀態轉換。

  1. 直接進入TERMINATED狀態,好比調用JDK已經不推薦使用的stop方法或者判斷某個邏輯標識。
  2. 進入BLOCKED狀態,好比調用了sleep,或者wait方法而加入了waitSet中。
  3. 進行某個阻塞的IO操做,好比因網絡數據的讀寫而進入了BLOCKED狀態。
  4. 獲取某個鎖資源,從而加入到該鎖的阻塞隊列中而進入了BLOCKED狀態。
  5. 因爲CPU的調度器輪詢使該線程放棄執行,進入RUNNABLE狀態。
  6. 線程主動調用yield方法,放棄CPU執行權,進入RUNNABLE狀態。

 四、線程的BLOCKED狀態

BLOCKED爲線程阻塞時的狀態,它能進入如下幾個狀態:

  1. 直接進入TERMINATED狀態,好比調用JDK已經不推薦使用的stop方法或者JVMCrash
  2. 線程阻塞的操做結束,好比讀取了想要的數據字節進入到RUNNABLE狀態。
  3. 線程完成了指定時間的休眠,進入到了RUNNABLE狀態
  4. Wait中的線程被其餘線程notify/notifyall喚醒,進入RUNNABLE狀態。
  5. 線程獲取到了某個鎖資源,進入RUNNABLE狀態。
  6. 線程在阻塞過程當中被打斷,好比其餘線程調用了interrupt方法,進入RUNNABLE狀態。

 五、線程的TERMINATED狀態

TERMINATED是一個線程的最終狀態,在該狀態中,線程將不會切換到其餘任何狀態,線程進入Terminated狀態意味着整個線程的生命週期結束了,下列狀況將會使線程進入Terminated狀態。

  1. 線程運行正常結束,結束生命週期。
  2. 線程運行出錯意外結束
  3. JVM崩潰,致使全部的線程都結束。

三、線程的start方法是什麼?

首先:Thread start源碼以下:

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 */
            }
        }
    }

    private native void start0();

start方法的源碼足夠簡單,其實最核心的部分是start0這個本地方法,也就是JNI方法;

也就是說在start方法中會調用start0方法,那麼重寫的那個run方法什麼時候被調用了呢?

實際上在開始執行這個線程的時候,JVM將會調用該線程的run方法,換言之,run方法是被JNI方法start0調用的,仔細閱讀start的源碼將會總結出以下幾個知識要點。

  1. Thread被構造後的NEW狀態,實際上threadStatus這個內部屬性爲0.
  2. 不能倆次啓動Thread,不然就會出現IllegalThreadStateException異常。
  3. 線程啓動後會被加入到一個ThreadGroup中。
  4. 一個線程生命週期的結束也就是到了terminated再次調用start方法是不容許的,也就是說Terminated狀態是沒有辦法回到runnable狀態的。

如執行如下代碼:

import java.util.concurrent.TimeUnit;

public class A {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(10);
                }catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        thread.start();
        thread.start();
    }
}

此時程序就會拋出

當咱們改下代碼,也就是生命週期結束後,再從新調用時。

import java.util.concurrent.TimeUnit;

public class A {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                }catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        thread.start();
        TimeUnit.SECONDS.sleep(5);
        thread.start();
    }
}

咱們會發現程序一樣會拋出illegalThread異常。

注意:程序雖然一樣會拋出異常,可是這倆個異常是有本質區別的。

  1. 第一個是由於重複啓動,只是第二次啓動時不容許的,可是此時線程是處於運行狀態的。
  2. 第二次企圖從新激活也拋出了非法狀態的異常,可是此時沒有線程,由於該線程的生命週期已經被終結。

經過以上分析咱們不難看出,線程真正的執行邏輯是在run方法中,一般咱們會把run方法成爲線程的執行單元。

若是咱們沒有重寫run,那run就是個空方法。

Thread的run和start是一個比較經典的模板設計模式,父類編寫算法結構代碼,子類實現邏輯細節,下面是一個簡單的模板設計模式。

package concurrent.chapter01;

public class TemplateMethod {
    public final void print(String message) {
        System.out.println("###");
        wrapPrint(message);
        System.out.println("###");
    }
    protected void wrapPrint(String message) {
        
    }
    public static void main(String[] args) {
        TemplateMethod t1 = new TemplateMethod() {
            @Override
            protected void wrapPrint(String message) {
                System.out.println("*"+message+"*");
            }
        };
        t1.print("Hello Thread");
        TemplateMethod t2 = new TemplateMethod() {
            @Override
            protected void wrapPrint(String message) {
                System.out.println("+"+message+"+");
            }
        };
        t2.print("Hello Thread");
    }
}

運行結果以下:

四、下面是一個模擬營業大廳叫號機的程序

假設共有4臺出號機,這就意味着有4個線程在工做,下面咱們用程序模擬一下叫號的過程,約定當天最多受理50筆業務,也就是說號碼最多能夠出到50

代碼以下:

package concurrent.chapter01;

public class TicketWindow extends Thread{
    private final String name;
    private static final int MAX = 50;
    private int index = 1;
    public TicketWindow(String name) {
        this.name=name;
    }
    @Override
    public void run() {
        while(index<=MAX) {
            System.out.println("櫃檯:"+name+" 當前號碼是:"+(index++));
        }
    }
    public static void main(String[] args) {
        TicketWindow t1 = new TicketWindow("一號初號機");
        t1.start();
        TicketWindow t2 = new TicketWindow("二號初號機");
        t2.start();
        TicketWindow t3 = new TicketWindow("三號初號機");
        t3.start();
        TicketWindow t4 = new TicketWindow("四號初號機");
        t4.start();
    }
}

運行結果以下:

顯然這不是咱們想看到的。如何改進呢?

這裏我將index設置爲staic變量

貌似有了改善。可是會出現線程安全問題。

因此Java提供了一個接口:Runnable專門用於解決該問題,將線程和業務邏輯的運行完全分離開。

五、Runnable接口的引入及策略模式

Runnalbe接口很是簡單,只是定義了一個無參數無返回值的run方法,具體代碼以下:

public interface Runnable{
  void run();    
}

在不少書中,都會說,建立線程有倆種方式,第一種是構造一個Thread,第二種是實現Runnable接口,這種說法是錯誤的,最起碼是不嚴謹的,在JDK中,表明線程的就只有Thread這個類,咱們在前面分析過,線程的執行單元就是run方法,你能夠經過繼承Thread而後重寫run方法實現本身的業務邏輯,也能夠實現Runnable接口實現本身的業務邏輯,代碼以下:

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

上面的代碼段是Thread run方法的源碼,咱們從中能夠去理解,建立線程只有一種方式,那就是構造Thread類,而實現線程的執行單元則有倆種方式,第一種是重寫Thread的run方法,第二種是實現Runnable接口的run方法,並將Runnable實例用做構造Thread的參數。

策略模式

其實不管是Runnable的run方法仍是,Thread自己的run方法都說想將線程的控制自己和業務邏輯的運行分離開,達到職責分明,功能單一的原則,這一點與GoF設計模式中的策略設計模式很相近。

以JDBC來舉例子:

package concurrent.chapter01;

import java.sql.ResultSet;

public interface RowHandler <T>{
    T handle(ResultSet set);
}

rowhandler接口只負責對從數據庫中查詢出來的結果集進行操做,至於最終返回成什麼樣的數據結構,須要本身去實現,相似於Runnable接口。

package concurrent.chapter01;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class RecordQuery {
    private final Connection connection;
    
    public RecordQuery(Connection connection) {
        this.connection = connection;
    }
    public<T> T query(RowHandler<T> handler,String sql,Object... params) throws SQLException{
        try(PreparedStatement stmt = connection.prepareStatement(sql)){
            int index = 1;
            for(Object param:params) {
                stmt.setObject(index++,param);
            }
            ResultSet resultSet = stmt.executeQuery();
            return handler.handle(resultSet);
        }
    }
}

上面的代碼的好處就是能夠用Query方法應對任何數據庫的查詢,返回結果的不一樣只會由於你傳入RowHandler的不一樣而不一樣,一樣RecodeQuery只負責數據的獲取,而RowHanlder則只負責數據的加工,職責分明,每一個類均功能單一。

重寫Thread類的run方法和實現Runnable接口的run方法是不能共享的,也就是說A線程不能把B線程的run方法看成本身的執行單元,而使用Runnable接口則很容易就能實現這一點即便用同一個Runnable的實例構造不一樣的實例。

若是不明白的話,看下面的代碼。

package concurrent.chapter01;

public class TicketWindowRunnable implements Runnable{
    private int index = -1;
    private final static int MAX = 50;
    @Override
    public void run() {
        while(index<=MAX) {
            System.out.println(Thread.currentThread()+" 的號碼是:"+(index++));
            try {
                Thread.sleep(100);
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        final TicketWindowRunnable task = new TicketWindowRunnable();
        Thread WindowThread1 = new Thread(task,"一號窗口");
        Thread WindowThread2 = new Thread(task,"二號窗口");
        Thread WindowThread3 = new Thread(task,"三號窗口");
        Thread WindowThread4 = new Thread(task,"四號窗口");
        WindowThread1.start();
        WindowThread2.start();
        WindowThread3.start();
        WindowThread4.start();
    }
}

運行結果以下:

 

 驚不驚喜?

上面並無對index進行static進行修飾,可是和上面被static修飾的是一個效果。緣由是咱們每次操做的都是同一個對象即task。

相關文章
相關標籤/搜索