Java 多線程編程

 

 

目錄

 

 

1、分享的目的:

2、使用多線程的意義

3、線程和進程的概念

 3.1、進程和線程的區別

    3.1.1、進程的特性

    3.1.2、線程的特性

4、線程的生命週期

5、線程的優先級

6、線程的創建方法 

7、線程池

7.1、newCachedThreadPool

7.2、newFixedThreadPool 

7.3、newScheduledThreadPool 

7.4、newSingleThreadExecutor

8、線程同步

 8.1  使用synchronized關鍵字

8.2  Synchronizing Statement (同步語句)

 

9、線程間通信

10、線程的死鎖

11、信號量

12、Thread 方法


1、分享的目的:

進一步掌握多線程編程和應用的技巧,對在平時的開發中應對高併發編程有所幫助。

2、使用多線程的意義

1)資源利用率更好

一個程序作爲一個進程來運行, 程序運行過程中能夠創建多個線程, 而一個線程在一個時刻只能運行在一個處理器核心上
2)程序設計在某些情況下更簡單
3)程序響應更快

多線程的代價:

1)設計更復雜
雖然有一些多線程應用程序比單線程的應用程序要簡單,但其他的一般都更復雜。在多線程訪問共享數據的時候,這部分代碼需要特別的注意。線程之間的交互往往非常複雜。不正確的線程同步產生的錯誤非常難以被發現,並且重現以修復。

2)上下文切換的開銷
當CPU從執行一個線程切換到執行另外一個線程的時候,它需要先存儲當前線程的本地的數據,程序指針等,然後載入另一個線程的本地數據,程序指針等,最後纔開始執行。這種切換稱爲“上下文切換”(“context switch”)。CPU會在一個上下文中執行一個線程,然後切換到另外一個上下文中執行另外一個線程。上下文切換並不廉價。如果沒有必要,應該減少上下文切換的發生。

3、線程和進程的概念

 

操作系統調度的最小單元是線程, 也叫輕量級進程(Light Weight Process) , 在一個進程裏可以創建多個線程, 這些線程都擁有各自的計數器、 堆棧和局部變量等屬性, 並且能夠訪問共享的內存變量。 處理器在這些線程上高速切換, 讓使用者感覺到這些線程在同時執行。

一個Java程序從main()方法開始執行, 然後按照既定的代碼邏輯執行, 看似沒有其他線程參與, 但實際上Java程序天生就是多線程程序, 因爲執行main()方法的是一個名 稱爲main的線程。 下面使用JMX來查看一個普通的Java程序包含哪些線程, 代碼如下:

public class MultiThread{
    public static void main(String[ ] args) {
        // 獲取Java線程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory. getThreadMXBean() ;
        // 不需要獲取同步的monitor和synchronizer信息,僅獲取線程和線程堆棧信息
        ThreadInfo[ ] threadInfos = 
            threadMXBean.dumpAllThreads(false, false) ;
        // 遍歷線程信息,僅打印線程ID和線程名稱信息
        for (ThreadInfo threadInfo : threadInfos) {
                System. out. println("[ " + threadInfo. getThreadId() + 
                    "] " + threadInfo.getThreadName()) ;
        }
    }
}

輸出結果如下(多次運行,結果可能不同): 



[4] Signal Dispatcher   //分發處理髮送給JVM信號的線程
[3] Finalizer           //調用對象finalize方法的線程
[2] Reference Handler  //清除Reference的線程
[1] main               //main線程,用戶程序入口

 

  • 進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間。
  • 線程是指進程中的一個執行流程(順序執行流),一個進程中可以運行多個線程,線程總是屬於某個進程,進程中的多個線程共享進程的內存。
  • 一個線程不能獨立的存在,它必須是進程的一部分。
  • 線程是進程的組成部分,一個進程可以有多個線程,一個線程在一個時刻只能運行在一個處理器核心上

 3.1、進程和線程的區別

    3.1.1、進程的特性


1) 獨立性:進程是系統中獨立存在的實體,它可以擁有自己獨立的資源,每個進程都擁有自己私有的地址空間,其他進程不能訪問這個進程空間內的數據。 
2) 動態性:進程與程序的區別在於,程序是靜態的,進程是動態的,程序只是一個靜態的指令集合,而進程是一個正在系統中運行的指令集合,有生命週期等時間概念; 
3) 併發性:進程之間,可以交替執行,提高程序執行效率。


    3.1.2、線程的特性

 
1) 進程之間不能共享內存,但線程可以共享同一片內存中的數據; 
2) 系統創建進程需要爲該進程重新分配系統資源,但創建線程的代價很小,因此用多線程實現多任務併發比多進程實現併發的效率高; 
3) java語言內置多線程功能支持,而不是單純的作爲底層操作系統的調度方式,Java封裝了操作系統底層的調度,屏蔽了不同操作系統調度之間的差異。

4、線程的生命週期

 

  • 新建狀態(New): 一個新產生的線程從新狀態開始了它的生命週期。它保持這個狀態直到程序start這個線程。
  • 就緒狀態(Ready):用戶調用 start() 方法之後,該線程就進入就緒狀態,但不是處於可運行狀態。當另一個線程給就緒狀態的線程發送信號時,該線程才重新切換到運行狀態。
  • 運行狀態(Running):當一個新狀態的線程被start以後,線程就變成可運行狀態,一個線程在此狀態下被認爲是開始執行其任務
  • 等待狀態(wait):當處於運行狀態下的線程調用 Thread 類的 wait() 方法時,該線程就會進入等待狀態。進入等待狀態的線程必須調用 Thread 類的 notify() 方法才能被喚醒。notifyAll() 方法是將所有處於等待狀態下的線程喚醒。
  • 休眠()狀態(Blocked): 由於一個線程的時間片用完了,該線程從運行狀態進入休眠狀態。當時間間隔到期或者等待的事件發生了,該狀態的線程切換到運行狀態。或者當線程調用 Thread 類中的 sleep() 方法時,則會進入休眠狀態。
  • 阻塞狀態:如果一個線程在運行狀態下發出輸入/輸出請求,該線程將進入阻塞狀態,在其等待輸入/輸出結束時,線程進入就緒狀態。對阻塞的線程來說,即使系統資源關閉,線程依然不能回到運行狀態。

           (一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中(wait會釋放持有的鎖)。

      (二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖

            (三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態(注意,sleep不會釋放線程所持有的鎖)。

  • 死亡狀態(Dead): 一個運行狀態的線程完成任務或者其他終止條件發生,該線程就切換到終止狀態或者當線程的 run() 方法執行完畢,線程進入死亡狀態。

使線程處於就緒狀態有如下幾種方法。

  • 調用 sleep() 方法。
  • 調用 wait() 方法。
  • 等待輸入和輸出完成。

 當線程處於就緒狀態後,可以用如下幾種方法使線程再次進入運行狀態。

  • 線程調用 notify() 方法。
  • 線程調用 notifyAll() 方法。
  • 線程調用 intermpt() 方法。
  • 線程的休眠時間結束。
  • 輸入或者輸出結束

5、線程的優先級

Java給每個線程安排優先級以決定與其他線程比較時該如何對待該線程。當只有一個線程時,優先級高的線程並不比優先權低的線程運行的快。相反,線程的優先級是用來決定何時從一個運行的線程切換到另一個。這叫“上下文轉換”(context switch)。決定上下文轉換髮生的規則很簡單:

  • 線程可以自動放棄控制。在I/O未決定的情況下,睡眠或阻塞由明確的讓步來完成。在這種假定下,所有其他的線程被檢測,準備運行的最高優先級線程被授予CPU。
  • 線程可以被高優先級的線程搶佔。在這種情況下,低優先級線程不主動放棄,處理器只是被先佔——無論它正在幹什麼——處理器被高優先級的線程佔據。基本上,一旦高優先級線程要運行,它就執行。這叫做有優先權的多任務處理。

每一個Java線程都有一個優先級,這樣有助於操作系統確定線程的調度順序。通過一個整型成員變量priority來控制優先級, 優先級的範圍從1~10, 在線程構建的時候可以通過setPriority(int)方法來修改優先級, 默認優先級是5, 優先級高的線程分配時間片的數量要多於優先級低的線程。

public final void setPriority(int newPriority);

如果要獲取當前線程的優先級,可以直接調用 getPriority() 方法。語法如下: 

public final int getPriority();

然而,線程優先級不能保證線程執行的順序,而且非常依賴於平臺。

6、線程的創建方法 


6.1、實現Runnable接口,然後將它傳遞給Thread的構造函數,創建一個Thread對象;

public class TaskClass implements Runnable {
    public TaskClass(...) {
    }
    // 實現Runnable中的run方法
    public void run() {
        // 告訴系統如何運行自定義線程
    }
}
 
public class Client {
    public void someMethod() {
        // 創建TaskClass的實例
        TaskClass task = new TaskClass(...);
        // 創建線程
        Thread thread = new Thread(task);
        // 啓動線程
        thread.start();
    }
}

6.2、直接繼承Thread類並重寫run方法,Thread實現Runnable,和實現Runnable原理一致,本質上也是實現了 Runnable 接口的一個實例。(不推薦使用:Java 不支持多繼承)


// 自定義thread類
public class CustomThread extends Thread {
    public CustomThread(...) {
    }
    // 重寫Runnable裏的run方法
    public void run() {
        // 告訴系統如何執行這個task
    }
}
 
// 自定義類
public class Client {
    public void someMethod() {
    // 創建一個線程
    CustomThread thread1 = new CustomThread(...);
    // 啓動線程
    thread1.start();
    // 創建另一個線程
    CustomThread thread2 = new CustomThread(...);
    // 啓動線程
    thread2.start();
    }
}

7、線程池

線程池用於高效地執行任務

如果需要爲一個任務創建一個線程,那麼用Thread類,如果有多個任務,最好用線程池,否則就要爲逐個任務創建線程,這種做法會導致低吞吐量和低性能。

Java通過Executors提供四種線程池,分別爲:

7.1、newCachedThreadPool

創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。

7.2、newFixedThreadPool 

創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。

7.3、newScheduledThreadPool 

創建一個大小無限制的線程池。此線程池支持定時以及週期性執行任務。

7.4、newSingleThreadExecutor

創建一個單線程的線程池。此線程池支持定時以及週期性執行任務。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    System.out.println(index);
                }
            });
        }
    }
}

Executor 接口用於在線程池中執行線程,ExecutorService 子接口用於管理和控制線程。

isTerminated() : 如果線程池中所有的任務都已終止,則返回true。

1. 使用 Executor 類中的靜態方法創建 Executor 對象。
2. newFixedThreadPool(int) 方法在池中創建固定數目的線程。如果一個線程結束執行一個任務,則可以重用來執行另一個任務。
3. 如果一個線程在shutdown前失敗,並且池中所有線程非idle狀態,還有新的任務等待執行,那麼新的線程會被創建以取代出錯的線程。
4. 如果池中所有線程非idle狀態,並且還有新的任務等待執行,newCachedThreadPool() 方法將用於創建新的線程。
5. 如果緩衝池中的線程超過60秒未被使用,則會被終止。緩衝池用來執行數目衆多的短任務時十分高效。

Executor併發執行3個線程: 


package testpackage;
import java.util.concurrent.*;
public class TaskThreadDemo {
    public static void main(String[] args) {
        // Create a fixed thread pool with maximum three threads
        ExecutorService executor = Executors.newFixedThreadPool(3);
        // Submit runnable tasks to the executor
        executor.execute(new PrintChar('a', 100));
        executor.execute(new PrintChar('b', 100));
        executor.execute(new PrintNum(100));
        // Shut down the executor
        executor.shutdown();
    }
} 


// 打印指定次數字符的 task
 class PrintChar implements Runnable {
    private char charToPrint; // The character to print
    private int times; // The number of times to repeat
 
    /** Construct a task with a specified character and number of
    * times to print the character
    */
    public PrintChar(char c, int t) {
        charToPrint = c;
        times = t;
    }
 
    @Override /** Override the run() method to tell the system
    * what task to perform
    */
    public void run() {
        for (int i = 0; i < times; i++) {
            System.out.print(charToPrint);
        }
    }
}
 
// The task class for printing numbers from 1 to n for a given n
class PrintNum implements Runnable {
    private int lastNum;
 
    /** Construct a task for printing 1, 2, ..., n */
    public PrintNum(int n) {
        lastNum = n;
    }
 
    @Override /** Tell the thread how to run */
    public void run() {
        for (int i = 1; i <= lastNum; i++) {
            System.out.print(" " + i);
        }
    }
}

如果將固定線程數由3改爲1:

ExecutorService executor = Executors.newFixedThreadPool(1);

3個任務將順序執行。如果改爲數目不固定,3個task將併發執行。

ExecutorService executor = Executors.newCachedThreadPool();

shutdown()則命令 executor關閉,之後不再接受新的任務,但如果有存在的線程,那麼這些線程將繼續執行直到結束。

 

8、線程同步

定義:當兩個或兩個以上的線程需要共享資源,它們需要某種方法來確定資源在某一刻僅被一個線程佔用。達到此目的的過程叫做同步(synchronization)。

Java中的同步塊用synchronized標記。同步塊在Java中是同步在某個對象上。所有同步在一個對象上的同步塊在同時只能被一個線程進入並執行操作。所有其他等待進入該同步塊的線程將被阻塞,直到執行該同步塊中的線程退出。

有四種不同的同步塊:

  1. 實例方法
  2. 靜態方法
  3. 實例方法中的同步塊
  4. 靜態方法中的同步塊

線程同步是爲了協調互相依賴的線程的執行。同步的關鍵是管程(也叫信號量semaphore)的概念。管程是一個互斥獨佔鎖定的對象,或稱互斥體(mutex)。在給定的時間,僅有一個線程可以獲得管程。當一個線程需要鎖定,它必須進入管程。所有其他的試圖進入已經鎖定的管程的線程必須掛起直到第一個線程退出管程。這些其他的線程被稱爲等待管程。一個擁有管程的線程如果願意的話可以再次進入相同的管程。

示例,創建100個線程,每個線程各往同一個銀行賬戶裏存1分錢,理論上全部線程執行完畢,賬戶結餘應爲100分,運行結果只有3分或其他不正確的結果等等。

package testpackage;
import java.util.concurrent.*;
 
public class TaskThreadDemo {
    
    private static Account account = new Account();
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
 
        // Create and launch 100 threads
        for (int i = 0; i < 100; i++) {
            executor.execute(new AddAPennyTask());
        }
        executor.shutdown();
        // Wait until all tasks are finished
        while (!executor.isTerminated()) {
        }
        System.out.println("What is balance? " + account.getBalance());
    }
    // A thread for adding a penny to the account
    private static class AddAPennyTask implements Runnable {
        public void run() {
            account.deposit(1);
        }
    }
 
    // An inner class for account
    private static class Account {
        private int balance = 0;
 
        public int getBalance() {
            return balance;
        }
 
        public void deposit(int amount) {
            int newBalance = balance + amount;
            // data-corruption problem and make it easy to see.
            try {
                Thread.sleep(5);
            }
            catch (InterruptedException ex) {
            }
            balance = newBalance;
        }
    }
}

出問題的是以下兩條語句,這兩條語句各個線程疊加訪問,造成訪問衝突,變量值沒有同步.  這兩條語句之間等待的時間越長,執行結果越不正確:

newBalance = balance + 1;balance = newBalance;

 8.1  使用synchronized關鍵字

爲了解決上面的問題,一種方法是使用synchronized關鍵字, 對臨界區加鎖,執行完成後釋放鎖。

public synchronized void deposit(double amount)

同步的方法在執行前請求鎖,如果是實例方法,對對象加鎖。如果是靜態方法,對類加鎖, 方法執行結束後釋放鎖。

8.2  Synchronizing Statement (同步語句)

也可以只對語句塊進行同步,語法:

synchronized (expr) {
   
 statements;
}

expr 必須爲對象的引用,上例中,相應語句修改如下,運行結果就是100.

synchronized (account) {
       account.deposit(1);
}

比起對整個method加Synchronized, 這種改法能提高併發性。
以下兩種寫法等價:

1.
public synchronized void xMethod() {
    // method body
}

2.
public void xMethod() {
    synchronized (this) {
        // method body
    }
}

 

9、線程間通信

爲避免輪詢,Java包含了通過wait( ),notify( )和notifyAll( )方法實現的一個進程間通信機制。這些方法在對象中是用final方法實現的,所以所有的類都含有它們。這三個方法僅在synchronized方法中才能被調用。儘管這些方法從計算機科學遠景方向上來說具有概念的高度先進性,實際中用起來是很簡單的:

  • wait( ) 告知被調用的線程放棄管程進入睡眠直到其他線程進入相同管程並且調用notify( )。
  • notify( ) 恢復相同對象中第一個調用 wait( ) 的線程。
  • notifyAll( ) 恢復相同對象中所有調用 wait( ) 的線程。具有最高優先級的線程最先運行。

10、線程的死鎖

需要避免的與多任務處理有關的特殊錯誤類型是死鎖(deadlock)。死鎖發生在當兩個線程對一對同步對象有循環依賴關係時。例如,假定一個線程進入了對象X的管程而另一個線程進入了對象Y的管程。如果X的線程試圖調用Y的同步方法,它將像預料的一樣被鎖定。而Y的線程同樣希望調用X的一些同步方法,線程永遠等待,因爲爲到達X,必須釋放自己的Y的鎖定以使第一個線程可以完成。死鎖是很難調試的錯誤,因爲:

  • 通常,它極少發生,只有到兩線程的時間段剛好符合時才能發生。
  • 它可能包含多於兩個的線程和同步對象(也就是說,死鎖在比剛講述的例子有更多複雜的事件序列的時候可以發生)。

11、信號量

信號量用於限制訪問共享資源的線程數

在計算機科學中,信號量是一個對象,它控制着對公共的訪問。訪問資源之前,線程必須從信號量獲得許可,訪問結束後,線程必須返還許可給信號量,如下圖所示:

爲了創建信號量,你必須指定許可數目,另fairness可選, 如下圖所示。一個task分別通過調用信號量的acquire()和release() 方法獲得許可和釋放許可。一旦許可被獲取,信號量的可用許可數減 1,被釋放則加 1。

許可數僅爲1 的信號量可用於模擬互斥鎖,下面的例子使用信號量以保證一個時間段內僅有一個線程可訪問deposit方法。一個線程在執行deposit方法時首先獲得許可,餘額更新後,線程釋放許可。永遠將release() 方法放在finally語句中是一種很好的做法,這種做法可保證即使出現了異常,許可最終可被釋放。

12、Thread 方法

下表列出了Thread類的一些重要方法:

序號 方法描述
1 public void start() 
使該線程開始執行;Java 虛擬機調用該線程的 run 方法。
2 public void run() 
如果該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作並返回。
3 public final void setName(String name) 
改變線程名稱,使之與參數 name 相同。
4 public final void setPriority(int priority) 
 更改線程的優先級。
5 public final void setDaemon(boolean on) 
將該線程標記爲守護線程或用戶線程。
6 public final void join(long millisec) 
等待該線程終止的時間最長爲 millis 毫秒。
7 public void interrupt() 
中斷線程。
8 public final boolean isAlive() 
測試線程是否處於活動狀態。活動狀態就是線程已經啓動且尚未終止。線程處於正在運行或準備開始運行的狀態,就認爲線程是“存活”的。

測試線程是否處於活動狀態。 上述方法是被Thread對象調用的。下面的方法是Thread類的靜態方法。

序號 方法描述
1

public static void yield() 
暫停當前正在執行的線程對象,並執行其他線程。

yieId() 方法的作用是放棄當前的 CPU 資源,將它讓給其他的任務去佔用 CPU 執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得 CPU 時間片。

2 public static void sleep(long millisec) 
在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),這個“正在執行的線程”是指 this.currentThread() 返回的線程。此操作受到系統計時器和調度程序精度和準確性的影響。
3 public static boolean holdsLock(Object x) 
當且僅當當前線程在指定的對象上保持監視器鎖時,才返回 true。
4 public static Thread currentThread() 
返回對當前正在執行的線程對象的引用。
5 public static void dumpStack() 
將當前線程的堆棧跟蹤打印至標準錯誤流。

join()方法、yield()方法和sleep()方法 

 

join()方法的作用:讓“主線程”等待“子線程”結束之後再繼續運行。這句話可能有點晦澀,我們還是通過例子去理解:

// 主線程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子線程
public class Son extends Thread {
    public void run() {
        ...
    }
}


  上面的有兩個類Father(主線程類)和Son(子線程類)。因爲Son是在Father中創建並啓動的,所以,Father是主線程類,Son是子線程類。在Father主線程中,通過new Son()新建一個“子線程s”。接着通過s.start()啓動“子線程s”,並且調用s.join()。在調用s.join()之後,Father主線程會一直等待,直到“子線程s”運行完畢;在“子線程s”運行完畢之後,Father主線程才能接着運行。這也就是我們所說的join()的作用,讓主線程等待,一直等到子線程結束之後,主線程才能繼續運行。。

  sleep()、yield()、join()等是Thread類的方法(而wait()和notify()是Object類的方法)。yield()方法是停止當前線程,讓同等優先權的線程運行。如果沒有同等優先權的線程,那麼yield()方法將不會起作用。

   sleep()使當前線程進入超時等待狀態(見上面的狀態轉移圖),所以執行sleep()的線程在指定的時間內肯定不會被執行;sleep()方法只讓出了CPU,而並不會釋放同步資源鎖。

   sleep()方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,yield()方法使當前線程讓出 CPU 佔有權,但讓出的時間是不可設定的。實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先級的線程處於同可運行狀態,如有,則把 CPU 的佔有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱爲“退讓”,它把運行機會讓給了同等優先級的其他線程。

   另外,sleep()方法允許較低優先級的線程獲得運行機會,但 yield() 方法執行時,當前線程仍處在可運行狀態,所以,不可能讓出較低優先級的線程些時獲得 CPU 佔有權。在一個運行系統中,如果較高優先級的線程沒有調用 sleep()方法,又沒有受到 I\O 阻塞,那麼,較低優先級的線程只能等待所有較高優先級的線程運行結束,纔有機會運行。
--------------------- 
參考鏈接:http://fuzhongmin05 

               :2222345345
               :阿凡盧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章