Java高級編程--多線程(二)

多線程程序將單個任務按照功能分解成多個子任務來執行,每一個子任務稱爲一個線程,多個線程共同完成主任務的運行過程,這樣能夠縮短用戶等待時間,提升服務效率。本篇博客將繼續介紹Java開發中多線程的使用。html

↷Java高級編程--多線程(一)java


目錄:算法

☍ 線程的生命週期編程

☍ 線程的同步設計模式

☍ 線程的通訊安全

☍ JDK5.0新增線程建立方式多線程


☍ 線程的生命週期

▾ Thread.State類定義了線程的幾種狀態

要想實現多線程,必須在主線程中建立新的線程對象。Java語言使用Thread類
及其子類的對象來表示線程,在它的一個完整的生命週期中一般要經歷以下的五種狀態
併發

新建: 當一個Thread類或其子類的對象被聲明並建立時,新生的線程對象處於新建狀態app

就緒:處於新建狀態的線程被start()後,將進入線程隊列等待CPU時間片,此時它已具有了運行的條件,只是沒分配到CPU資源ide

運行:當就緒的線程被調度並得到CPU資源時,便進入運行狀態,run()方法定義了線程的操做和功能

阻塞:在某種特殊狀況下,被人爲掛起或執行輸入輸出操做時,讓出 CPU 並臨時中
止本身的執行,進入阻塞狀態

死亡:線程執行完了全部工做或線程被提早強制性地停止或出現異常致使結束


☍ 線程的同步

多個線程執行的不肯定性引發執行結果的不穩定。多個線程對資源的共享,會形成操做的不完整性,會破壞數據。此時須要線程的同步來解決問題。

如窗口售票:

售票代碼:

class SellTicket extends Thread{
    private static int ticketNum = 100;
    public SellTicket(String windownName){
        super.setName(windownName);
    }
    @Override
    public void run() {
        while(true){
            if(ticketNum > 0){
                try {
                    sleep(100);  //阻塞,放大線程安全問題
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName() + "窗口賣票,票號爲:" + ticketNum--);
            }else{
                System.out.println("票已賣光");
                break;
            }
        }
    }
}
public class Thread_SellTicket {
    public static void main(String[] args) {
        SellTicket t1 = new SellTicket("窗口1");
        SellTicket t2 = new SellTicket("窗口2");
        SellTicket t3 = new SellTicket("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

結果(存在線程安全問題):

▾ 線程安全問題的探析(同步機制)

提出問題:線程安全問題,如賣票的多線程中,出現了重票和錯票

問題緣由:當某個線程操做還沒有完成時,其餘線程進入操做了共享的資源,致使共享數據的錯誤

解決思路:當一個線程a在操做共享資源時,其餘線程不能參與進來, 直到線程a操做完成時,其餘線程才能夠開始操做共享資源。這種狀況即便線程a出現了阻塞也不能改變。

解決方法:在Java中,經過同步機制來解決線程的安全問題(synchronized)

☃ 方法一:同步代碼塊

☃ 方法二:同步方法

同步機制一:同步代碼塊

synchronized (同步監視器/鎖:對象){
	// 須要被同步的代碼;
}

//如在窗口售票例子中經過Runnable接口實現的多線程中使用同步代碼塊
public void run() {
        while(true){
            synchronized(this){//synchronized(obj){
                //this指當前線程對象,在runnable實現方式中惟一(runnable對象傳入Thread構造器中需惟一)
                if(ticketNum > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "窗口賣票,票號爲:" + ticketNum--);
                }else{
                    System.out.println("票已賣光");
                    break;
                }
            }
        }
    }

//如在窗口售票例子中經過繼承Thread類實現的多線程中使用同步代碼塊
public void run() {
        while(true){
            synchronized(TS_SellTicket.class){ //synchronized(obj){  類的class也是對象,TS_SellTicket爲繼承Thread的子類
                if(ticketNum > 0){
                    try {
                        sleep(100);//阻塞
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "窗口賣票,票號爲:" + ticketNum--);
                }else{
                    System.out.println("票已賣光");
                    break;
                }
            }
        }
    }

同步機制二:同步方法

若是操做的共享數據的代碼完整的聲明在一個方法中,能夠將這個方法聲明爲同步的

權限修飾符 synchronized 方法名(){//操做共享數據的代碼}
    
//如在窗口售票例子中經過Runnable接口實現的多線程中使用同步方法
    public void run() {
        while(true){
            printInfo();
            if (ticketNum<=0){
                System.out.println("票已售完");
                break;
            }
        }
    }
    public synchronized void printInfo(){
        if(ticketNum > 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "窗口賣票,票號爲:" + ticketNum--);
        }
    }
    
//如在窗口售票例子中經過繼承Thread類實現的多線程中使用同步代方法
    public void run() {
        while(true){
            printInfo();
            if(ticketNum <= 0){
                System.out.println("票已售光");
            }
        }
    }
//    public synchronized void printInfo(){   同步監視器不惟一
    public static synchronized void printInfo(){  //同步監視器默認爲當前類(Thread子類.class)
        try {
            sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(ticketNum > 0){
            System.out.println(Thread.currentThread().getName() + "窗口賣票,票號爲:" + ticketNum--);
        }
    }

synchronized的監視器/鎖

☄ 任意對象均可以做爲同步鎖

☄ 同步方法的鎖(默認):靜態方法(類名.class)、非靜態方法(this)

☄ 同步代碼塊:本身指定(如Object obj),不少時候也是指定爲this(runnable接口實現類方式)或類名.class(Thread繼承方式)

說明

☃ 操做共享數據的代碼爲須要同步的代碼,不能多也不能少

☃ 共享數據:多個線程共同操做的變量。(售票例子中爲票ticket)

☃ 同步監視器/同步鎖:任何一個類的對象均可以充當鎖。

➥ 多個線程必須共用一把鎖(同一個對象)

➥ 一個線程類中的全部靜態方法共用同一把鎖(類名.class),全部非靜態方
法共用同一把鎖(this),同步代碼塊(指定需謹慎)

肯定同步機制的使用

◌ 一、代碼是否存在線程安全?

  • 明確哪些代碼是多線程運行的代碼
  • 明確多個線程是否有共享數據
  • 明確多線程運行代碼中是否有語句操做共享數據

◌ 二、代碼存在線程安全如何解決?

  • 對多條操做共享數據的語句,只能讓一個線程都執行完,在執行過程當中,其餘線程不能夠參與執行。即全部操做共享數據的這些語句都要放在同步範圍中

◌ 二、注意選中要同步的代碼範圍

  • 範圍過小:沒鎖住全部有安全問題的代碼
  • 範圍太大:沒發揮多線程的功能。

同步鎖的釋放

☃ 當前線程的同步方法/同步代碼塊執行結束或執行了線程對象的stop()方法。

☃ 當前線程在同步代碼塊、同步方法中遇到break/return終止了該代碼塊/該方法的繼續執行。

☃ 當前線程在同步代碼塊/同步方法中出現了未處理的Error或Exception,致使異常結束。

☃ 當前線程在同步代碼塊/同步方法中執行了線程對象的wait()方法,當前線程暫停,並釋放鎖。

不會釋放鎖的說明

➥ 線程執行同步代碼塊或同步方法時,程序調用Thread.sleep()、Thread.yield()方法暫停當前線程的執行,此時只是讓出了cpu資源,當前線程並未結束

➥ 線程執行同步代碼塊時,其餘線程調用了該線程的suspend()方法將該線程掛起,該線程不會釋放鎖(同步監視器),需注意的是應儘可能避免使用suspend()和resume()來控制線程

單例模式之懶漢式(線程安全補充)

單列模式詳解見連接↷Java面向對象--單例(Singleton)設計模式和main方法

class Bank extends Thread{
    private Bank(){}   //私有化構造器
    private static Bank instance = null;   //私有靜態對象變量,先步new出實例對象
//    public static synchronized Bank getInstance(){
    public static Bank getInstance(){
        //提供獲取私用靜態對象的方法,若靜態單例對象爲null,new出實例對象,不然直接返回
        if (instance == null) {  //效率更高
            synchronized (Bank.class) {
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }

    @Override
    public void run() {
        getInstance();
    }
}
public class Singleton_ThreadSave {
    public static void main(String[] args) {

    }
}

線程的死鎖問題

◌ 死鎖緣由

  不一樣的線程分別佔用對方須要的同步資源,而且都在等待對方放棄本身須要的同步資源,此時就造成了線程的死鎖

◌ 死鎖狀態

  出現死鎖後,不會出現異常,不會有錯誤提示,只是全部線程都處於阻塞狀態,沒法繼續運行,程序沒法結束

◌ 解決方法

專門的算法邏輯;儘可能減小同步資源的定義;儘可能避免多層嵌套同步

死鎖代碼演示:

public class Dead_Lock {
    public static void main(String[] args) {
        dead_lock();
    }
    public static void dead_lock(){
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    s1.append("c");
                    s2.append("3");
                    synchronized (s2){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

       //使用匿名對象建立線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    s1.append("a");
                    s2.append("1");
                    synchronized (s1){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

同步機制三:Lock鎖

☃ 從JDK 5.0開始,Java提供了更強大的線程同步機制——經過顯式定義同步鎖對象來實現同步。同步鎖使用Lock對象充當。

☃ java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源以前應先得到Lock對象

☃ ReentrantLock 類實現了 Lock ,它擁有與synchronized相同的併發性和內存語義,在實現線程安全的控制中,比較經常使用的是ReentrantLock,能夠顯式加鎖、釋放鎖。

Lock鎖使用代碼演示:

class L_Thread implements Runnable{
    private int ticket = 100;
    //一、定義lock鎖
    private ReentrantLock lock = new ReentrantLock(true);
    //參數爲boolean型,線程是否先到先執行,不會出現一個線程連續執行屢次
    @Override
    public void run() {
        while (true){
            try{
                //二、調用lock鎖
                lock.lock();
                if (ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "窗口賣票,票號爲:" + ticket--);
                }else {
                    break;
                }
            }finally {
                //三、調用unlock釋放鎖
                lock.unlock(); 
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        L_Thread l = new L_Thread();
        Thread t1 = new Thread(l,"窗口1");
        Thread t2= new Thread(l,"窗口2");
        Thread t3 = new Thread(l,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

注意: 加鎖後不要忘記釋放鎖,若是同步代碼有異常,要將unlock()寫入finally語句塊

synchronized與Lock的對比

◌ 相同點

 兩者都能解決線程的安全問題

◌ 不一樣點

  • Lock是顯式鎖(lock須要手動的啓動同步鎖(lock()),同時結束同步也須要手動調用unlock()釋放鎖),synchronized是隱式鎖(執行時自動啓動同步監視器/鎖,在執行完相應的同步代碼之後/出了做用域自動釋放同步監視器/鎖)
  • Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
  • 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。而且具備
    更好的擴展性(提供更多的子類)

◌ 三種同步機制的使用優先級(視具體狀況而定)

 Lock鎖 ➠ 同步代碼塊(已經進入了方法體,分配了相應) ➠ 同步方法
(在方法體以外)


☍ 線程的通訊

 線程的通訊經過wait()方法和notify()/notifyAll()方法實現

經過線程通訊使用線程1和線程2交替打印1-100:

交替打印也可經過ReentrantLock類的ReentrantLock(true)構造器實現

class TC_Thread implements Runnable{
    private int index = 1;
    @Override
    public void run() {
        while(true){
            synchronized (this){
                notify();    //喚醒被wait的線程
                if (index <= 100) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + index++);
                    try {
                        wait();   //阻塞當前線程
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else{
                    break;
                }
            }
        }
    }
}
public class ThreadCommunication {
    public static void main(String[] args) {
        TC_Thread tc = new TC_Thread();
        Thread t1 = new Thread(tc,"線程1");
        Thread t2 = new Thread(tc,"線程2");
        t1.start();
        t2.start();
    }
}

wait()與notify()和notifyAll()

由於這三個方法必須由鎖對象調用,而任意對象均可以做爲synchronized的同步鎖,
所以這三個方法聲明在Object類中

wait():一旦執行此方法,令當前線程掛起並放棄CPU、同步資源進行等待,使別的線程可訪問並修改共享資源,當前線程就進入阻塞狀態,並釋放同步監視器,等候其餘線程調用notify()或notifyAll()方法喚醒,喚醒後等待從新得到對監視器的全部權後才能繼續執行。

notify():一旦執行此方法,就會喚醒被wait的一個線程,若是有多個線程被wait,就喚醒優先級高的線程

notifyAll():一旦執行此方法,就會喚醒被wait的全部線程

wait()方法的使用

☃ 在當前線程中調用方法:對象名.wait()。

☃ 使當前線程進入等待(某對象)狀態,直到另外一線程對該對象發出 notify(或notifyAll)爲止。。

☃ 調用方法的必要條件:當前線程必須具備對該對象的監控權(加鎖)。

☃ 調用此方法後,當前線程將釋放對象監控權,而後進入等待。

☃ 在當前線程被notify後,要從新得到監控權,而後從斷點處繼續代碼的執行。

notify()/notifyAll()方法的使用

☃ 在當前線程中調用方法: 對象名.notify()。

☃ 功能:喚醒等待該對象監控權的一個/全部線程。

☃ 調用方法的必要條件:當前線程必須具備對該對象的監控權(加鎖)。

注意:這三個方法只有在synchronized方法或synchronized代碼塊中才能使用,不然會報
java.lang.IllegalMonitorStateException異常

注意:wait()、notify()、notifyAll()三個方法的調用者必須與同步代碼塊或同步方法中的同步監視器/鎖相同,不然會出現 IllegalMonitorStateException異常

sleep()和wait()方法的異同點

◌ 相同點

 一旦執行方法,均可以使得當前線程進入阻塞狀態

◌ 不一樣點

兩個方法的聲明位置不一樣:Thread類中聲明sleep(),Object類中聲明wait();

調用場景不一樣:sleep()能夠在任何須要的場合調用,wait()只能在synchronized同步代碼塊或同步方法中調用

是否釋放同步監視器:若是兩個方法都使用在synchronized中,sleep()不會自動釋放鎖,wait()會自動釋放鎖


☍ JDK5.0新增線程建立方式

▾ 實現Callable接口方式建立線程

Runnable接口和Callable接口對比

◌ 相比run方法,call()方法有返回值,在須要獲取返回值的狀況下很適用

◌ 能夠拋出異常從而被外層的操做獲取異常信息

◌ callable是支持泛型的返回值

◌ Callable須要藉助FutureTask類,好比獲取返回結果

Future接口

◌ 能夠對具體Runnable、Callable任務的執行結果進行取消、查詢是
否完成、獲取結果等。

◌ FutrueTask是Futrue接口的惟一的實現類

◌ FutureTask 同時實現了Runnable、Future接口。它既能夠做爲
Runnable被線程執行,又能夠做爲Future獲得Callable的返回值

Callable接口方式實現線程步驟

↬ 建立Callable實現類,定義Callable實現類對象c

↬ 定義FutureTask對象f,將Callable實現類對象c做爲FutureTask構造器的參數

↬ 定義Thread線程對象t,將FutureTask對象f做爲Thread構造器的參數

↬ Thread對象t調用start()方法啓動線程

↬ 若要得到Callable實現類對象的call()方法返回值,需啓動線程後使用FutureTask對象f調用get()方法

Callable接口實現多線程代碼演示:

//一、建立Callable實現類
class CT_Thread implements Callable {
    //二、實現call()方法
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class CallableTest {
    public static void main(String[] args) {
        //三、定義Callable接口實現類對象
        CT_Thread ct = new CT_Thread();
        //四、定義FutureTask對象,並將Callable接口實現類對象做爲FutureTask構造器的參數
        FutureTask fu= new FutureTask(ct);   //FutureTask構造器參數爲Callable實現類對象
        //五、定義Thread對象,將FutureTask對象做爲參數傳遞到Thread類的構造器中,Thread對象調用start()方法啓動線程
        Thread t1 = new Thread(fu);
        t1.start();
        try {
            //六、FutureTask對象的get()方法返回值爲構造器參數Callable實現類重寫的call()的返回值(可省略此步,須要獲取call()方法返回值時寫)
            Object sum = fu.get();
            System.out.println("總和爲:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

▾ 線程池方式建立線程

常常建立和銷燬、使用量特別大的資源,好比並髮狀況下的線程,對性能影響很大。提早建立好多個線程,放入線程池中,使用時直接獲取,使用完
放回池中。能夠避免頻繁建立銷燬、實現重複利用。相似生活中的公共交通工具。

使用線程池的優勢

◌ 提升響應速度(減小了建立新線程的時間)

◌ 下降資源消耗(重複利用線程池中線程,不須要每次都建立)

◌ 便於線程管理(提供了不少方法)

  ↬ corePoolSize:核心池的大小

  ↬ maximumPoolSize:最大線程數

  ↬ keepAliveTime:線程沒有任務時最多保持多長時間後會終止

  ↬ ......

線程池相關API

☃ JDK 5.0起提供了線程池相關API:ExecutorService 和 Executors。

☃ ExecutorService:真正的線程池接口。常見子類ThreadPoolExecutor。

   ↬ void execute(Runnable command) :執行任務/命令,沒有返回值,通常用來執行
Runnable

   ↬  Future submit(Callable task):執行任務,有返回值,通常又來執行
Callable

   ↬ void shutdown() :關閉鏈接池

☃ Executors:工具類、線程池的工廠類,用於建立並返回不一樣類型的線程池。

  ↬ Executors.newCachedThreadPool():建立一個可根據須要建立新線程的線程池

↬ Executors.newFixedThreadPool(n); 建立一個可重用固定線程數的線程池

↬ Executors.newSingleThreadExecutor() :建立一個只有一個線程的線程池

↬ Executors.newScheduledThreadPool(n):建立一個線程池,它可安排在給定延遲後運
行命令或者按期地執行。

線程池方式建立線程代碼演示:

class NumberThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

public class ThreadPool {
    public static void main(String[] args) {
        //一、提供指定線程數量的線程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        //設置線程池的屬性
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        service1.setCorePoolSize(15);  //核心池的大小
        service1.setMaximumPoolSize(10); //最大線程數
        //二、執行指定線程的操做,,須要提供實現Runnable接口或Callable接口實現類的對象做爲execute的參數
        service.execute(new NumberThread());   //適合於Runnable
        service.execute(new NumberThread1());

        //service.submit();     //適合於Callable

        service.shutdown();   //關閉線程池
    }
}

本博客與CSDN博客༺ཌ༈君☠纖༈ད༻同步發佈

相關文章
相關標籤/搜索