1. 程式人生 > 其它 >Java併發程式設計基礎

Java併發程式設計基礎

併發與並行

併發與並行的區別?

併發:同時完成多個任務,無需等待當前任務完成即可執行其它任務。例如解決IO密集型任務。

並行:同時在多個CPU中執行多個任務,將任務分為多個部分,在多個CPU中執行多個子任務。用於解決CPU密集型任務。

總之,併發(並行)是為了減少等待(阻塞)時間的技術

舉個例子來說明單執行緒,併發,並行的區別:

小明正在聽歌,她媽媽讓他做作業,這時,小明...

  • 先把歌聽完了,再開始寫作業,小明就是單執行緒的
  • 暫停歌曲播放,開始寫作業,1分鐘後停止寫作業,開始播放音樂,如此反覆,小明就是併發的
  • 一邊聽歌,一邊寫作業,小明就是並行的

什麼時候應該使用併發?

  • 永遠不要使用併發
  • 除非程式執行的慢到無法忍受的地步
  • 除非沒有條件更換具有更好效能的機器的時候
  • 除非沒有更好的資料結構和演算法的時候
  • 再考慮使用併發
  • 不要自己實現,優先考慮使用成熟的併發庫
  • 但是,你無法避免併發,必須理解它

多執行緒機制

CPU時間分片機制,CPU將輪流給每個任務分配其佔用的時間。這個過程由執行緒排程器自動控制。

Java併發程式設計的實現

Runnable介面

定義Runnable介面的實現類,作為子執行緒的任務

class ARunnable implements Runnable
{
    @Override
    public void run()
    {
        // TODO
    }
}

建立Thread例項,傳入任務

Thread t=new Thread(new ARunnable());
t.start();

可以使用匿名內部類或者lambda表示式進行簡化

new Thread(()->{
    // TODO
}).start();

Thread類

定義Thread的派生類,重寫其run方法

Thread本身就實現了Runnable介面,所以其run方法實際上是Runnable介面中的

class AThread extends Thread
{
    @Override
    public void run()
    {
        // TODO
    }
}

建立Thread例項,啟動執行緒

Thread t = new AThread();
t.start();

與Runnable一樣,也可以進行簡寫

new Thread(()->{
    // TODO
}).start();

Callable介面

使用Runnable定義任務時,執行完畢後不能返回任何值。如果需要在任務中返回值,可以使用Callable介面。

定義Callable介面的實現類,重寫call()方法

class ACallable implements Callable<String>
{
    @Override
    public String call()
    {
        return "over";
    }
}

使用執行器Executor的submit()方法執行任務

ExecutorService executor = Executors.newCachedThreadPool();
ACallable task = new ACallable();
Future<String> future = executor.submit(task);

返回Future型別的物件,使用阻塞的get()方法獲取返回值

try
{
    System.out.println(future.get());
}
catch(InterruptedException | ExecutionException e)
{
    e.printStackTrace();
}
executor.shutdown();

Fork-Join框架

Java SE7中添加了fork-join框架,專門處理可以分解為子任務的情況,如下所示

if(任務規模小於規定的值)
{
    直接處理
}
else
{
    分解為子任務;
    遞迴處理每一個子任務;
    合併結果;
}

使用步驟:

  • 定義任務

    如果任務有返回值,擴充套件RecursiveTask;如果任務沒有返回值,擴充套件RecursiveAction。它們都是ForkJoinTask的抽象子類,重寫compute()方法

  • 建立ForkJoinPool物件

  • 呼叫invoke()方法執行任務

  • 呼叫join()方法獲取結果

示例:統計隨機生成的double陣列中,值大於0.5的數字個數

class Counter extends RecursiveTask<Integer>
{
    private static final int THRESHOLD=10000;
    private double[] nums;
    private int from;
    private int to;

    public Counter(double[] nums,int from,int to)
    {
        this.nums=nums;
        this.from=from;
        this.to=to;
    }

    @Override
    protected Integer compute()
    {
        if(to-from<THRESHOLD) // 直接執行
        {
            int count=0;
            for(int i = from;i < to;i++)
            {
                if(nums[i]>0.5)
                {
                    count++;
                }
            }
            return count;
        }
        else
        {
            //分解為子任務
            int mid=(from+to)/2;
            Counter first = new Counter(nums,from,mid);
            Counter second = new Counter(nums,mid,to);
            //遞迴執行子任務
            invokeAll(first,second);
            //合併結果
            return first.join()+ second.join();
        }
    }
}
ForkJoinPool pool=new ForkJoinPool();
Counter counter=new Counter(nums,0,nums.length);
pool.invoke(counter);
int count = counter.join();

Java執行緒生命週期與管理

執行緒休眠

靜態的sleep()方法會丟擲中斷異常,必須在子執行緒中捕獲處理,因為異常不能跨執行緒丟擲

try
{
    //Thread.sleep(1000L);
    TimeUnit.MILLISECONDS.sleep(1000L);
}
catch(InterruptedException e)
{
    e.printStackTrace();
}

執行緒優先順序

執行緒的優先順序表示該執行緒的重要性,排程器傾向於讓優先順序更高的執行緒優先執行。然而,這種行為是不確定的。因此,不要依賴於優先順序進行任何假設。

Java中執行緒優先順序從低到高分為10個等級(1-10),預設為5

  • void setPriority(int newPriority)
  • int getPriority()

執行緒讓步

靜態的yield()方法給執行緒排程器一個暗示,表示當前任務完成的差不多了,可以讓具有相同優先順序的其它執行緒優先執行。
但是,執行緒排程器並不保證按照暗示執行,所以,不要依賴於yield()進行任何假設。

  • void yield()

後臺執行緒

後臺執行緒也稱守護執行緒,用於在程式執行的過程中,在後臺提供通用的服務。
當所有非後臺執行緒結束時,程式也就終止了,同時自動殺死所有的後臺執行緒。main執行緒不是後臺執行緒。

  • void setDaemon(boolean on)
  • boolean isDaemon()

必須線上程啟動之前呼叫setDaemon()方法。

如果是一個後臺執行緒,那麼它建立的任何執行緒都被自動設定為後臺執行緒。

後臺執行緒會在main方法終止後“突然”關閉,即使是finally程式碼快中的程式碼也不一定會被執行。

執行緒的加入

線上程A中呼叫執行緒B的join()方法,執行緒A將被掛起,等到執行緒B執行完畢後,執行緒A再從掛起的地方繼續執行。join()方法可以帶上一個引數,表示掛起的時間。

  • void join()
  • void join(long millis)
  • void join(long millis, int nanos)

join()方法內部使用wait()方法實現。

中斷執行緒

呼叫interrupt()方法可以中斷執行緒。

當在一個被阻塞的執行緒上呼叫interrupt()時,會丟擲異常InterruptedException。

被中斷的執行緒可以決定如何響應中斷,是處理完異常後繼續執行,還是中止執行。

靜態的interrupted()或者非靜態isInterrupted()方法用於檢測當前執行緒的中斷狀態。呼叫前者會清除該執行緒的中斷狀態。如果捕獲了中斷異常,其中斷狀態總是返回false

  • void interrupt()
  • boolean isInterrupted()
  • boolean interrupted()

執行緒狀態(宣告週期)

Java中定義了執行緒的6種狀態,這6種狀態定義在Thread內部列舉類State中

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  1. 新建(NEW):當執行緒被new時的狀態,此時它已經分配了必需的系統資源,並執行了初始化。
  2. 就緒(RUNNABLE):呼叫start()方法後,執行緒準備就緒,只要排程器分配了時間片給它,就可以開始執行任務
  3. 阻塞(BLOCKED):執行緒嘗試獲取鎖時的狀態
  4. 等待(WAITING):當執行緒呼叫了非計時的wait()或join()方法後,處於等待狀態
  5. 計時等待(TIMED_WAITING):當先呼叫了sleep()或計時的wait(),join()方法後,處於計時等待狀態
  6. 終止(TERMINATED):執行緒執行完畢或者由於未捕獲的異常而終止

可以使用getState()方法獲取執行緒狀態,返回列舉型別的執行緒狀態

  • State getState()