Java多線程

1、多進程與多線程

多進程是指多個進程在單個處理器上併發執行,CPU在某個時間點,只能執行一個程序,CPU計算較快,多個進程輪換使用CPU使用戶感覺好像多個進程同步執行。

而多線程則是擴展了多進程的概念,同一個進程可以併發處理多個任務,進程之間的系統資源是相互獨立的,而線程之間共享父進程的系統資源,而自身有獨立的堆棧、計數器和局部變量,使用起來有一定的優勢,但也要更加小心。

多線程的優勢:

  • 共享內存方便
  • 創建線程的代價小

2、線程的創建方法

Java有三種方法創建線程:繼承Thread類、實現Runnable、實現Callable接口。

繼承Thread類

  • 重寫Thread中的run()方法,這是線程執行體
  • 通過Thread的對象的start()方法啓動線程,然後執行run方法
  • Thread.currentThread()是Thread的類方法,返回的是正在執行的線程對象
  • getName是Thread類的實例方法,返回線程的名字

實現Runnable接口

通過實現Runnable接口創建線程其實也是通過Thread對象創建的,新建Thread對象時,通過構造器:Thread(target:Runnable, name:String),傳入一個target參數,其中Runnable是一個函數式接口,只有一個run()的抽象方法,重寫這個方法,作爲線程的執行體。
這個方法與繼承Thread類的主要區別是:

  • 在繼承Thread類的run()方法中,this即爲當前運行的線程,而Runnable接口中的run()方法,則需要通過類方法Thread.currentThread()得到當前線程。
  • 新建不同的線程,可以傳入相同的target參數,所以通過實現Runnable接口可以共享target中的變量。而繼承Thread類的線程實例之間是相互獨立的。

實現Callable接口

Callable與Runnale類似,都是傳入target參數作爲線程的執行體,但是Callable與Runnale的區別在於,Callable可以帶有返回值,不是通過run方法,而是通過call方法作爲線程的執行體。

Callable也是一個函數式接口,它的抽象方法是Call,這個作爲線程的執行體,但是Callable接口不是Runnable接口,所以不能直接傳入。Java設計了FutureTask類,該類實現了Future方法,用於接受call方法返回值,還實現了Runnable接口,可以作爲target傳入。新建FutureTask類時傳入一個實現了Callable的對象,把Callable對象包裝起來,通過FutureTask實例對象的get方法可以獲得call函數的返回值。

三種方法的對比:

  • 通過Runnable、Callable接口新建線程,還可以繼承其他的類,編程更爲靈活
  • 通過Runnable、Callable接口新建線程,可以多個線程共享一個target對象。
  • 一般情況下推薦優先使用接口

3、線程的生命週期

在這裏插入圖片描述

5種狀態:

  • 新建:線程創建後進入新建狀態
  • 就緒:線程創建後調用start方法進入就緒狀態,開始等待cpu調度執行
  • 運行:當cpu調度就緒狀態的線程,得到系統分配的資源,開始執行就進入了運行狀態。
  • 阻塞:因爲某些原因放棄系統資源,從運行狀態進入阻塞狀態,直到阻塞條件結束,重新進入就緒狀態,才能再次等待被cpu調度。
  • 死亡:執行完成、出現異常、通過stop方法(容易造成死鎖)線程會死亡。

4、控制線程

  • join:join是Thread的實例方法,某個執行中的方法中 調用a.join方法,則當前執行的線程會被阻塞,直到a線程執行結束再重新進入就緒狀態。
  • 後臺線程:通過實例方法a.setDaemon(true)可以將線程設置爲後臺線程,後臺線程的特徵是:當前臺線程都死亡時,後臺線程隨之死亡。
  • sleep:是Thread的類方法,調用後當前的線程進入阻塞狀態,直到sleep時間結束。
  • yield:也是Thread的類方法,調用後線程放棄系統資源,但是不是阻塞,而是進入就緒狀態。
  • 線程的優先級:就緒狀態的線程,cpu會根據優先級調度,通過setPriority、getPriority可以設置和獲取線程對象的優先等級,有幾個常量:MAX_PRIORITY(10)、MIN_PRIORITY(1)、NORM_PRIORITY(5)

5、線程同步

線程同步是指當一個線程對內存地址進行操作時,其他線程不能對這個地址進行操作。

Java中線程同步的方法主要有兩個,一個是隱式的同步鎖,通過synchronized對代碼塊或方法進行同步,另一個顯式的同步鎖,Lock類新建同步鎖的對象,通過實例方法lock和unlock手動的進行上鎖和解鎖。

synchronized

它可以修飾一個代碼塊:

synchronized(obj){
	// 同步的代碼塊
}

obj是同步監視器,阻止多個線程在代碼塊執行時對同步監視器對象併發訪問。

synchronized 也可以修飾一個方法:

public synchronized void test(){}

修飾方法時,同步監視器就是this,阻止多個線程在方法執行時對同步監視器對象併發訪問。

同步監視器的釋放:

  • break、return終止了代碼塊
  • Error和Exception停止
  • 調用了wait()方法暫停

不會釋放:

  • sleep() 、yield()會暫停執行線程,但是不會釋放同步監視器
  • suspend() 將線程掛起也不會釋放同步監視器。

Lock

用Lock類顯式定義同步鎖,可以更靈活的使用同步鎖,一個Lock對象與一個對象對應,同一個時刻只有一個線程能進入臨界區。

一般使用try-finally結構寫同步鎖的代碼,確保必要時釋放鎖。

class X{
	private final ReentrantLock lock = new ReentrantLock();
	public void m(){
		// 加鎖
		lock.lock(); 
		try{
			// 需要保證線程安全的代碼
		}
		// finally塊確保釋放鎖
		finally{
			lock.unlock();
		}
	}
}

讀寫鎖之間,讀和寫會相互阻塞,實際情況中,讀比寫出現的頻率多很多,這會造成寫線程的飢餓現象,StampedLock則是Java 8中改進讀寫鎖的一個類,它讀寫之間不會相互阻塞,只是在讀的時候監測有沒有寫的操作,如果有的話重讀。但是寫與寫之間還是會相互阻塞。

6、線程之間的通信

線程的調度對於程序是透明的,如果線程之間的訪問有先後順序,需要線程間的通信機制,確保線程按順序協調運行。

Java主要有三種線程間通信的方法:

  • 對於synchronized修飾:使用Object類自帶的wait()、notify()、notifyAll() 方法
  • 對於使用同步鎖的:使用Condition類的await()、signal()、signalAll()方法
  • 使用阻塞隊列BlockingQueue

synchronized 修飾

  • wait() : 線程進入等待,且釋放同步監視器,要等待notify或notifyAll喚醒線程。
  • notify() : 喚醒這個同步監視器上一個線程,如果有多個就隨機選一個。
  • notifyAll() : 喚醒這個同步監視器上所有線程。

synchronized修飾有兩種情況,一種是修飾代碼塊,一種是修飾方法。

修飾代碼塊時,同步監視器是(obj)內的對象obj,使用同步監視器對象的wait()方法讓線程等待,線程進入阻塞狀態,等待同步監視器對象調用notify()或notifyAll()喚醒線程,線程通信有InterruptedException,代碼需要放在try-catch模塊中手動處理異常。

修飾方法時,同步監視器是this,所以使用this.wait() this可以省略,和notify()、notifyAll()來阻塞、喚醒線程。

使用同步鎖時

使用同步鎖時,沒有隱式的同步監視器,而是一個lock對象綁定一個臨界區的對象,lock上鎖防止多個線程進入臨界區。使用同步鎖時,使用Condition對象實現線程間的通信,在lock對象上綁定Condition對象,Condition對象有類似於wati、notify、notifyAll方法的await(),signal(),signalAll()方法。

  • await() : 當前線程進入等待,直到其他線程調用該線程Condition對象的signal或signalAll方法
  • signal() : 喚醒這個lock對象上的一個線程。
  • signalAll() : 喚醒這個lock對象上的所有線程。
// 在類中綁定同步鎖
private final Lock lock = new ReentranLock();
// 同步鎖綁定Condition
private final Condition cond = lock.newCondition();

// 在try-catch中,cond調用await()控制線程等待阻塞
// 別的線程調用該cond的signal()方法喚醒線程

阻塞隊列BlockingQueue

以隊列的數據結構,當隊列滿發生放操作時,線程會被阻塞,當隊列空發生拿操作時,線程也會被阻塞。

拋出異常 不同返回值 阻塞線程 超過指定市場
隊尾插入元素 add(e) offer(e) put(e) offer(e, time, time)
隊頭刪除元素 remove() poll() take() poll(time, unit)
獲取,不刪除元素 element() peek()

7、線程池

由於新建線程需要一定的成本,可以創建一定數量的空閒線程,形成線程池,向線程池傳入run()、call()的方法,執行完後線程不死亡,重新回到線程池中成爲空閒狀態。

// 創建線程池
ExecutorService pool = Executors.newFixedThreadPool(6);

// 向線程池中提交線程任務
pool.submit(target);
pool.shutdown();

對於多核cpu,我們可以將任務分解爲多個小任務,Java8中的ForkJoinPool線程池支持分拆多個小任務,調用ForkJoinPool的submit(ForkJoinTask task)啓動線程池,其中ForkJoinTask代表一個可並行、合併的任務,它有兩個抽象子類RecursiveAction 和 RecursiveTask,其中RecursiveAction代表沒有返回值的任務,RecursiveTask代表有返回值的任務。

RecursiveAction,用遞歸的方式,將大任務分解爲小任務,下面是一個打印0-n,每50個數一個線程的樣例。

class PrintTask extends RecursiveAction{
    private static final int THRESHOLD = 50;
    private int start;
    private int end;
    public PrintTask(int start, int end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected void compute() {
        if(end - start < THRESHOLD){
            for(int i = start; i < end; i++){
                System.out.println(Thread.currentThread().getName() + "i的值是" + i);
            }
        }
        else{
            int middle = (start + end) / 2;
            PrintTask left = new PrintTask(start, middle);
            PrintTask right = new PrintTask(middle, end);
            left.fork();
            right.fork();
        }
    }
}

RecursiveTask同樣也使用遞歸的方式將任務分解,它可以接受返回值,將返回值進行合併。

class CalTask extends RecursiveTask<Integer>{
    private static final int THRESHOLD = 20;
    private int arr[];
    private int start;
    private int end;

    public CalTask(int[] arr, int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        if(end - start < THRESHOLD){
            for(int i = start; i < end; i++){
                sum += arr[i];
            }
            return sum;
        }
        else{
            int middle = (start + end) / 2;
            CalTask left = new CalTask(arr, start, middle);
            CalTask right = new CalTask(arr, start, middle);
            left.fork();
            right.fork();
            return left.join() + right.join();
        }
    }
}

8、線程相關類

TreadLocal類

TreadLocal類會每個線程複製一份數據,每個線程享有一份相互獨立的局部變量,有三個public的方法:

  • T get() : 返回線程副本中的局部變量
  • void remove() : 刪除線程的局部變量中當前線程的值
  • void set (T value) : 設置線程局部變量的值

包裝線程不安全的集合

利用Collections中的synchronizedCollection方法,可以將某個Collection包裝爲線程安全的集合

Collection<T> c = Collections.synchronizedCollection(new Collection<>());

// 例如HashMap對象:
HashMap hm = Collections.synchronizedHashMap(new HashMap());
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章