天天看點

Java多線程Java中線程的3種建立方式:多線程三種同步方式線程間通信線程池

Java多線程

  • Java中線程的3種建立方式:
  • 多線程三種同步方式
  • 線程間通信
  • 線程池
    • 線程池的建立
    • 向線程池送出任務
    • 關閉線程池

Java中線程的3種建立方式:

線程啟動start() 和 中斷線程interrupt()

1.繼承Thread類

建立自定義線程類并繼承Thread類–>重寫run()–>執行個體化自定義線程類–>調用自定義實作類的start()啟動線程。

public class ThreadDemo extends Thread {
    @Override//重寫run()
    public void run() {
        System.out.println(Thread.currentThread().getName()+" subthread");
    }
}
public class App {
    public static void main(String[] args) {
       System.out.println(Thread.currentThread().getName()+" main");//列印目前線程(主線程)的線程名
        ThreadDemo threadDemo=new ThreadDemo();//執行個體化自定義的線程類
        threadDemo.start();//開啟線程
    }
}
           

2.實作Runnable接口

建立自定義線程類并實作Runnable接口–>實作Runnable中的run()–>執行個體化自定義的線程類–>将建立的執行個體當作Thread類構造方法的參數來執行個體化Thread類–>調用Thread類執行個體的start()方法啟動線程。

public class RunnableDemo implements Runnable {//建立自定義線程類
    @Override//實作run()
    public void run() {//一個線程所有的操作都要在run()中
        System.out.println(Thread.currentThread().getName()+" subthread");//列印目前線程線程名
    }
}

public class App {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+" main");//列印目前線程(主線程)的線程名
        RunnableDemo runnable = new RunnableDemo();//執行個體化自定義線程類
        Thread thread=new Thread(runnable);//執行個體化Thread類
        thread.start();//開啟一個線程
    }
}
           

3.實作Callable接口

建立自定義線程類并實作Callable類–>實作call()方法–>執行個體化自定義線程類–>将自定義線程類的執行個體當作參數來執行個體化FutureTask類–>将FutureTask類的執行個體當作Thread類構造方法的參數來執行個體化Thread類–>調用Thread類執行個體的start()方法啟動線程。

import java.util.concurrent.Callable;

public class CallableDemo implements Callable<Integer> {
    @Override//實作call()
    public Integer call() throws Exception {
        int i=100;
        System.out.println(Thread.currentThread().getName()+" subthread");
        return i;//傳回目前線程的執行結果
    }
}

import java.util.concurrent.*;

public class App {
    public static void main(String[] args) {
       System.out.println(Thread.currentThread().getName()+" main");//列印目前線程(主線程)的線程名
        CallableDemo callable=new CallableDemo();//執行個體化自定義線程類
        FutureTask<Integer> result = new FutureTask<>(callable);
        Thread thread = new Thread(result);
        thread.start();
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
           

三種方法對比

1.繼承Thread類為單繼承,而實作接口為多實作,是以實作接口方式在使用上比較靈活。

2.實作接口建立的線程可以放入線程池來管理,而繼承Thread類建立的線程不可以放入線程池。

3.Thread類底層實作了Runnable接口,底層也實作了run()方法,我們繼承Thread類需要重寫該方法;而實作Runnable接口需要實作run()方法。可以試一下,當我們繼承Thread類時,不重寫run()方法不會報錯,而實作Runnable接口,不實作run()會報錯。

4.繼承Thread類的線程類的執行個體直接調用start()方法去啟動該線程;

實作Runnable接口的線程類的執行個體需要作為參數來執行個體化Thread類,然後再由Thread類執行個體調用start()去啟動線程;

5.實作Callable接口的線程類的執行個體需要作為參數來執行個體化FutureTask類,然後再把FutureTask類執行個體作為參數來執行個體化Thread類,然後再由Thread類執行個體調用start()去啟動線程。

繼承Thread類和實作runnable接口建立的線程的操作都在run()方法中;

實作Callable接口建立的線程操作都在call()方法中,而且在實作接口時可以指定一個參數類型作為call()傳回值的類型,call()方法是一個有傳回值而且抛異常的方法。傳回值由FutureTask負責擷取。

總結:繼承Thread類這種方法使用起來比較友善,但底層還是實作Runnable接口;實作Runnable接口的方法啟動線程需要用Thread類的start()方法或放線上程池中啟動;實作Callable接口的方法也需要用Thread類的start()方法或放線上程池中啟動,而且需要先執行個體化FutureTask,但可以得到線程執行的傳回值。

多線程三種同步方式

  1. synchronized

    synchronized關鍵字修飾的方法,由于java的每個對象都有一個内置鎖,當用此關鍵字修飾方法時,内置鎖會保護整個方法。在調用該方法前,需要獲得内置鎖,否則就處于阻塞狀态。

    synchronized關鍵字也可以修飾靜态方法,此時如果調用該靜态方法,将會鎖住整個類。

    synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上内置鎖,進而實作同步。

    注:同步是一種高開銷的操作,是以應該盡量減少同步的内容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。

  2. volatile 隻保證可見性,不保證原子性

    a.volatile關鍵字為域變量的通路提供了一種免鎖機制

    b.使用volatile修飾域相當于告訴虛拟機該域可能會被其他線程更新

    c.是以每次使用該域就要重新計算,而不是使用寄存器中的值

    d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量

  3. 使用重入鎖實作線程同步

    在JavaSE5.0中新增了一個java.util.concurrent包來支援同步。ReentrantLock類是可重入、互斥、實作了Lock接口的鎖, 它與使用synchronized方法和快具有相同的基本行為和語義,并且擴充了其能力。

    ReenreantLock類的常用方法有:

    ReentrantLock() : 建立一個ReentrantLock執行個體

    lock() : 獲得鎖

    unlock() : 釋放鎖

    如果synchronized關鍵字能滿足使用者的需求,就用synchronized,因為它能簡化代碼 。如果需要更進階的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖。

線程間通信

  1. wait與notify

    wait():使一個線程處于等待狀态,并且釋放所持有的對象的lock。

    sleep():使一個正在運作的線程處于睡眠狀态,是一個靜态方法,調用此方法要捕捉InterruptedException異常。

    notify():喚醒一個處于等待狀态的線程,注意的是在調用此方法的時候,并不能确切的喚醒某一個等待狀态的線程,而是由JVM确定喚醒哪個線程,而且不是按優先級。

    Allnotity():喚醒所有處入等待狀态的線程,注意并不是給所有喚醒線程一個對象的鎖,而是讓它們競争。

線程池

在開發過程中,合理地使用線程池能夠帶來3個好處。

  • 降低資源消耗。通過重複利用已建立的線程降低線程建立和銷毀造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要等到線程建立就能立即執行。
  • 提高線程的可管理性。線程是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一配置設定、調優和監控。但是,要做到合理利用線程池,必須對其實作原理了如指掌。
    Java多線程Java中線程的3種建立方式:多線程三種同步方式線程間通信線程池
    從圖中可以看出,當送出一個新任務到線程池時,線程池的處理流程如下。
  • 線程池判斷核心線程池裡的線程是否都在執行任務。如果不是,則建立一個新的工作線程來執行任務。如果核心線程池裡的線程都在執行任務,則進入下個流程。
  • 線程池判斷工作隊列是否已經滿。如果工作隊列沒有滿,則将新送出的任務存儲在這個工作隊列裡。如果工作隊列滿了,則進入下個流程。
  • 線程池判斷線程池的線程是否都處于工作狀态。如果沒有,則建立一個新的工作線程來執行任務。如果已經滿了,則交給飽和政策來處理這個任務。

線程池的建立

我們可以通過ThreadPoolExecutor來建立一個線程池。

ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
                unit, workQueue, threadFactory, handler);
           

建立一個線程池時需要輸入幾個參數:

  • corePoolSize(線程池的基本大小):當送出一個任務到線程池時,線程池會建立一個線程來執行任務,即使其他空閑的基本線程能夠執行新任務也會建立線程,等到需要執行的任務數大于線程池基本大小時就不再建立。

    如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前建立并啟動所有基本線程。

  • maximumPoolSize(線程池最大數量):線程池允許建立的最大線程數。如果隊列滿了,并且已建立的線程數小于最大線程數,則線程池會再建立新的線程執行任務。值得注意的是,如果使用了無界的任務隊列這個參數就沒什麼效果。
  • keepAliveTime(線程活動保持時間):線程池的工作線程空閑後,保持存活的時間。是以,如果任務很多,并且每個任務執行的時間比較短,可以調大時間,提高線程的使用率。
  • TimeUnit(線程活動保持時間的機關),可選的機關有:

    天(DAYS)

    小時(HOURS)

    分鐘(MINUTES)

    毫秒(MILLISECONDS)

    微秒(MICROSECONDS,千分之一毫秒)

    納秒(NANOSECONDS,千分之一微秒)

  • workQueue(任務隊列):用于儲存等待執行的任務的阻塞隊列。

    可以選擇以下幾個阻塞隊列:

    ArrayBlockingQueue:是一個基于數組結構的有界阻塞隊列,此隊列按FIFO(先進先出)原則對元素進行排序。

    LinkedBlockingQueue:一個基于連結清單結構的阻塞隊列,此隊列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。靜态工廠方法Executors.newFixedThreadPool()使用了這個隊列。

    SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀态,吞吐量通常要高于LinkedBlockingQueue,靜态工廠方法Executors.newCachedThreadPool使用了這個隊列。

    PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。

  • ThreadFactory:用于設定建立線程的工廠,可以通過線程工廠給每個建立出來的線程設定更有意義的名字。使用開源架構guava提供的ThreadFactoryBuilder可以快速給線程池裡的線程設定有意義的名字,代碼如下:
  • RejectedExecutionHandler(飽和政策):當隊列和線程池都滿了,說明線程池處于飽和狀态,那麼必須采取一種政策處理送出的新任務。這個政策預設情況下是AbortPolicy,表示無法處理新任務時抛出異常。在JDK 1.5中Java線程池架構提供了以下4種政策。

    AbortPolicy:直接抛出異常。

    CallerRunsPolicy:隻用調用者所線上程來運作任務。

    DiscardOldestPolicy:丢棄隊列裡最近的一個任務,并執行目前任務。

    DiscardPolicy:不處理,丢棄掉。

    當然,也可以根據應用場景需要來實作RejectedExecutionHandler接口自定義政策。如記錄日志或持久化存儲不能處理的任務。

向線程池送出任務

可以使用兩個方法向線程池送出任務,分别為execute()和submit()方法。

  • execute()方法用于送出不需要傳回值的任務,是以無法判斷任務是否被線程池執行成功。通過以下代碼可知execute()方法輸入的任務是一個Runnable類的執行個體。
threadsPool.execute(new Runnable() {
    @Override
    public void run() {
        // TODO Auto-generated method stub
    }
});
           
  • submit()方法用于送出需要傳回值的任務。線程池會傳回一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,并且可以通過future的get()方法來擷取傳回值,get()方法會阻塞目前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞目前線程一段時間後立即傳回,這時候有可能任務沒有執行完。
Future<Object> future = executor.submit(harReturnValuetask);
try {
    Object s = future.get();
} catch (InterruptedException e) {
    // 進行中斷異常
} catch (ExecutionException e) {
    // 處理無法執行任務異常
} finally {
    // 關閉線程池
    executor.shutdown();
}
           

關閉線程池

可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。

它們的原理是周遊線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,是以無法響應中斷的任務可能永遠無法終止。

但是它們存在一定的差別:

  • shutdownNow首先将線程池的狀态設定成STOP,然後嘗試停止所有的正在執行或暫停任務的線程,并傳回等待執行任務的清單,
  • shutdown隻是将線程池的狀态設定成SHUTDOWN狀态,然後中斷所有沒有正在執行任務的線程。

隻要調用了這兩個關閉方法中的任意一個,isShutdown方法就會傳回true。當所有的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會傳回true。

至于應該調用哪一種方法來關閉線程池,應該由送出到線程池的任務特性決定:

  • 通常調用shutdown方法來關閉線程池。
  • 如果任務不一定要執行完,則可以調用shutdownNow方法。