Java基礎學習筆記: 多線程,同步鎖

多線程介紹

學習多線程以前,咱們先要了解幾個關於多線程有關的概念。
進程:進程指正在運行的程序。確切的來講,當一個程序進入內存運行,即變成一個進程,進程是處於運行過程當中的程序,而且具備必定獨立功能。java

線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是能夠有多個線程的,這個應用程序也能夠稱之爲多線程程序。編程

簡而言之:一個程序運行後至少有一個進程,一個進程中能夠包含多個線程安全

什麼是多線程呢?即就是一個程序中有多個線程在同時執行。
經過下圖來區別單線程程序與多線程程序的不一樣:
單線程程序:即,如有多個任務只能依次執行。當上一個任務執行結束後,下一個任務開始執行。如,去網吧上網,網吧只能讓一我的上網,當這我的下機後,下一我的才能上網。
多線程程序:即,如有多個任務能夠同時執行。如,去網吧上網,網吧可以讓多我的同時上網。多線程

程序運行原理

分時調度

全部線程輪流使用 CPU 的使用權,平均分配每一個線程佔用 CPU 的時間。併發

搶佔式調度

優先讓優先級高的線程使用 CPU,若是線程的優先級相同,那麼會隨機選擇一個(線程隨機性),Java使用的爲搶佔式調度。dom

搶佔式調度詳解jvm

大部分操做系統都支持多進程併發運行,如今的操做系統幾乎都支持同時運行多個程序。好比:如今咱們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開着畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,」感受這些軟件好像在同一時刻運行着「。編輯器

實際上,CPU(中央處理器)使用搶佔式調度模式在多個線程間進行着高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對咱們的感受要快,看上去就是在同一時刻運行。
其實,多線程程序並不能提升程序的運行速度,但可以提升程序運行效率,讓CPU的使用率更高。ide

主線程

回想咱們之前學習中寫過的代碼,當咱們在dos命令行中輸入java空格類名回車後,啓動JVM,而且加載對應的class文件。虛擬機並會從main方法開始執行咱們的程序代碼,一直把main方法的代碼執行結束。若是在執行過程遇到循環時間比較長的代碼,那麼在循環以後的其餘代碼是不會被立刻執行的。以下代碼演示:函數

複製代碼
package thread;

class Demo{
    String name;
    Demo(String name){
        this.name = name;
    }
    void show()    {
        for (int i=1;i<=10000 ;i++ )    {
            System.out.println("name="+name+",i="+i);
        }
    }
}

class ThreadDemo {
    public static void main(String[] args) {
        Demo d = new Demo("小強");
        Demo d2 = new Demo("旺財");
        d.show();
        d2.show();
        System.out.println("Hello World!");
    }
}
複製代碼

若在上述代碼中show方法中的循環執行次數不少,這時在d.show();下面的代碼是不會立刻執行的,而且在dos窗口會看到不停的輸出name=小強,i=值,這樣的語句。爲何會這樣呢?

緣由是:jvm啓動後,必然有一個執行路徑(線程)從main方法開始的,一直執行到main方法結束,這個線程在java中稱之爲主線程。當程序的主線程執行時,若是遇到了循環而致使程序在指定位置停留時間過長,則沒法立刻執行下面的程序,須要等待循環結束後可以執行。那麼,可否實現一個主線程負責執行其中一個循環,再由另外一個線程負責其餘代碼的執行,最終實現多部分代碼同時執行的效果?

可以實現同時執行,經過Java中的多線程技術來解決該問題。

Thread類

該如何建立線程呢?經過API中搜索,查到Thread類。經過閱讀Thread類中的描述。Thread是程序中的執行線程。Java 虛擬機容許應用程序併發地運行多個執行線程。

構造方法

 

經常使用方法

繼續閱讀,發現建立新執行線程有兩種方法。

  • 一種方法是將類聲明爲 Thread 的子類。該子類應重寫 Thread 類的 run 方法。建立對象,開啓線程。run方法至關於其餘線程的main方法。
  • 另外一種方法是聲明一個實現 Runnable 接口的類。該類而後實現 run 方法。而後建立Runnable的子類對象,傳入到某個線程的構造方法中,開啓線程。

建立線程方式一繼承Thread類

建立線程的步驟:

  • 1 定義一個類繼承Thread。
  • 2 重寫run方法。
  • 3 建立子類對象,就是建立線程對象。
  • 4 調用start方法,開啓線程並讓線程執行,同時還會告訴jvm去調用run方法。
複製代碼
package thread;
//測試類 public class Demo01 { public static void main(String[] args) { //建立自定義線程對象 MyThread mt = new MyThread("新的線程!"); //開啓新線程 mt.start(); //在主方法中執行for循環 for (int i = 0; i < 10; i++) { System.out.println("main線程!"+i); } } } //線程類 class MyThread extends Thread { //定義指定線程名稱的構造方法 public MyThread(String name) { //調用父類的String參數的構造方法,指定線程的名稱 super(name); } /** * 重寫run方法,完成該線程執行的邏輯 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName()+":正在執行!"+i); } } }
複製代碼

線程對象調用 run方法和調用start方法區別?線程對象調用run方法不開啓線程。僅是對象調用方法。線程對象調用start開啓線程,並讓jvm調用run方法在開啓的線程中執行。

繼承Thread類原理

咱們爲何要繼承Thread類,並調用其的start方法才能開啓線程呢?
繼承Thread類:由於Thread類用來描述線程,具有線程應該有功能。那爲何不直接建立Thread類的對象呢?以下代碼:

Thread t1 = new Thread();
t1.start();//這樣作沒有錯,可是該start調用的是Thread類中的run方法,而這個run方法沒有作什麼事情,更重要的是這個run方法中並無定義咱們須要讓線程執行的代碼。

建立線程的目的是什麼?

是爲了創建程序單獨的執行路徑,讓多部分代碼實現同時執行。也就是說線程建立並執行須要給定線程要執行的任務。

對於以前所講的主線程,它的任務定義在main函數中。自定義線程須要執行的任務都定義在run方法中。

Thread類run方法中的任務並非咱們所須要的,只有重寫這個run方法。既然Thread類已經定義了線程任務的編寫位置(run方法),那麼只要在編寫位置(run方法)中定義任務代碼便可。因此進行了重寫run方法動做。

多線程的內存圖解
多線程執行時,到底在內存中是如何運行的呢?以上個程序爲例,進行圖解說明:

多線程執行時,在棧內存中,其實每個執行線程都有一片本身所屬的棧內存空間。進行方法的壓棧和彈棧。當執行線程的任務結束了,線程自動在棧內存中釋放了。可是當全部的執行線程都結束了,那麼進程就結束了。

獲取線程名稱

開啓的線程都會有本身的獨立運行棧內存,那麼這些運行的線程的名字是什麼呢?該如何獲取呢?既然是線程的名字,按照面向對象的特色,是哪一個對象的屬性和誰的功能,那麼咱們就去找那個對象就能夠了。查閱Thread類的API文檔發現有個方法是獲取當前正在運行的線程對象。還有個方法是獲取當前線程對象的名稱。既然找到了,咱們就能夠試試。

  • Thread.currentThread()獲取當前線程對象
  • Thread.currentThread().getName();獲取當前線程對象的名稱
複製代碼
class MyThread extends Thread { //繼承Thread
    MyThread(String name){
        super(name);
    }
    //複寫其中的run方法
    public void run(){
        for (int i=1;i<=20 ;i++ ){
            System.out.println(Thread.currentThread().getName()+",i="+i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
//建立兩個線程任務
        MyThread d = new MyThread("d");
        MyThread d2 = new MyThread("d2");
        d.run();//沒有開啓新線程, 在主線程調用run方法
        d2.start();//開啓一個新線程,新線程調用run方法
    }
}
複製代碼

經過結果觀察,原來主線程的名稱:main;自定義的線程:Thread-1,線程多個時,數字順延。如Thread-1......

進行多線程編程時,不要忘記了Java程序運行是從主線程開始,main方法就是主線程的線程執行內容。

建立線程方式—實現Runnable接口

建立線程的另外一種方法是聲明實現 Runnable 接口的類。該類而後實現 run 方法。而後建立Runnable的子類對象,傳入到某個線程的構造方法中,開啓線程。
爲什麼要實現Runnable接口,Runable是啥玩意呢?繼續API搜索。
查看Runnable接口說明文檔:Runnable接口用來指定每一個線程要執行的任務。包含了一個 run 的無參數抽象方法,須要由接口實現類重寫該方法。

接口中的方法

Thread類構造方法

建立線程的步驟。

  • 一、定義類實現Runnable接口。
  • 二、覆蓋接口中的run方法。。
  • 三、建立Thread類的對象
  • 四、將Runnable接口的子類對象做爲參數傳遞給Thread類的構造函數。
  • 五、調用Thread類的start方法開啓線程。

代碼演示:

複製代碼
package thread;

public class Demo03 {
    public static void main(String[] args) {
//建立線程執行目標類對象
        Runnable runn = new MyRunnable();
//將Runnable接口的子類對象做爲參數傳遞給Thread類的構造函數
        Thread thread = new Thread(runn);
        Thread thread2 = new Thread(runn);
//開啓線程
        thread.start();
        thread2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main線程:正在執行!"+i);
        }
    }
}
//自定義線程執行任務類
 class MyRunnable implements Runnable{
    //定義線程要執行的run方法邏輯
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("個人線程:正在執行!"+i);
        }
    }
}
複製代碼

實現Runnable的原理

爲何須要定一個類去實現Runnable接口呢?繼承Thread類和實現Runnable接口有啥區別呢?

實現Runnable接口,避免了繼承Thread類的單繼承侷限性。覆蓋Runnable接口中的run方法,將線程任務代碼定義到run方法中。

建立Thread類的對象,只有建立Thread類的對象才能夠建立線程。線程任務已被封裝到Runnable接口的run方法中,而這個run方法所屬於Runnable接口的子類對象,因此將這個子類對象做爲參數傳遞給Thread的構造函數,這樣,線程對象建立時就能夠明確要運行的線程的任務。

實現Runnable的好處

第二種方式實現Runnable接口避免了單繼承的侷限性,因此較爲經常使用。實現Runnable接口的方式,更加的符合面向對象,線程分爲兩部分,一部分線程對象,一部分線程任務。繼承Thread類,線程對象和線程任務耦合在一塊兒。一旦建立Thread類的子類對象,既是線程對象,又有線程任務。實現runnable接口,將線程任務單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務進行解耦。

線程的匿名內部類使用

使用線程的內匿名內部類方式,能夠方便的實現每一個線程執行不一樣的線程任務操做。
方式1:建立線程對象時,直接重寫Thread類中的run方法

複製代碼
package thread;

public class Demo04 {
    public  static  void  main(String[]args){
        new Thread() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()+ "...X...." + x);
                }
            }
        }.start();
    }
}
複製代碼

方式2:使用匿名內部類的方式實現Runnable接口,從新Runnable接口中的run方法

複製代碼
      Runnable r = new Runnable() {
            public void run() {
                for (int x = 0; x < 40; x++) {
                    System.out.println(Thread.currentThread().getName()
                            + "...Y...." + x);
                }
            }
        };
        new Thread(r).start();
複製代碼

線程池

線程池,其實就是一個容納多個線程的容器,其中的線程能夠反覆使用,省去了頻繁建立線程對象的操做,無需反覆建立線程而消耗過多資源。

咱們詳細的解釋一下爲何要使用線程池?

在java中,若是每一個請求到達就建立一個新線程,開銷是至關大的。在實際使用中,建立和銷燬線程花費的時間和消耗的系統資源都至關大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。除了建立和銷燬線程的開銷以外,活動的線程也須要消耗系統資源。若是在一個jvm裏建立太多的線程,可能會使系統因爲過分消耗內存或「切換過分」而致使系統資源不足。爲了防止資源不足,須要採起一些辦法來限制任何給定時刻處理的請求數目,儘量減小建立和銷燬線程的次數,特別是一些資源耗費比較大的線程的建立和銷燬,儘可能利用已有對象來進行服務。

線程池主要用來解決線程生命週期開銷問題和資源不足問題。經過對多個任務重複使用線程,線程建立的開銷就被分攤到了多個任務上了,並且因爲在請求到達時線程已經存在,因此消除了線程建立所帶來的延遲。這樣,就能夠當即爲請求服務,使用應用程序響應更快。另外,經過適當的調整線程中的線程數目能夠防止出現資源不足的狀況。

使用線程池方式--Runnable接口

一般,線程池都是經過線程池工廠建立,再調用線程池中的方法獲取線程,再經過線程去執行任務方法。 
Executors:線程池建立工廠類

public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象

ExecutorService:線程池類

Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,並執行
Future接口:用來記錄線程任務執行完畢後產生的結果。線程池建立與使用

使用線程池中線程對象的步驟:

  • 建立線程池對象
  • 建立Runnable接口子類對象
  • 提交Runnable接口子類對象
  • 關閉線程池

代碼演示:

複製代碼
package thread;

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

public class ThreadPoolDemo {
    public static void main(String[] args) {
//建立線程池對象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
//建立Runnable實例對象
        MyRunnable r = new MyRunnable();

//本身建立線程對象的方式
//Thread t = new Thread(r);
//t.start(); ---> 調用MyRunnable中的run()

//從線程池中獲取線程對象,而後調用MyRunnable中的run()
        service.submit(r);
//再獲取個線程對象,調用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
//注意:submit方法調用結束後,程序並不終止,是由於線程池控制了線程的關閉。將使用完的線程又歸還到了線程池中

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

//Runnable接口實現類
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一個教練");

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教練來了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完後,教練回到了游泳池");
    }
}
複製代碼

使用線程池方式—Callable接口

Callable接口:與Runnable接口功能類似,用來指定線程的任務。其中的call()方法,用來返回線程任務執行完畢後的結果,call方法可拋出異常。

ExecutorService:線程池類

<T> Future<T> submit(Callable<T> task):獲取線程池中的某一個線程對象,並執行線程中的call()方法

Future接口:用來記錄線程任務執行完畢後產生的結果。線程池建立與使用

  • 使用線程池中線程對象的步驟:
  • 建立線程池對象
  • 建立Callable接口子類對象
  • 提交Callable接口子類對象
  • 關閉線程池

代碼演示:

複製代碼
package thread;

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

public class ThreadPoolDemo02 {
    public static void main(String[] args) {
//建立線程池對象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
//建立Callable對象
        MyCallable c = new MyCallable();

//從線程池中獲取線程對象,而後調用MyRunnable中的run()
        service.submit(c);

//再獲取個教練
        service.submit(c);
        service.submit(c);
//注意:submit方法調用結束後,程序並不終止,是由於線程池控制了線程的關閉。將使用完的線程又歸還到了線程池中

//關閉線程池
//service.shutdown();
    }
}
//Callable接口實現類,call方法可拋出異常、返回線程任務執行完畢後的結果
 class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        System.out.println("我要一個教練:call");
        Thread.sleep(2000);
        System.out.println("教練來了: " +Thread.currentThread().getName());
        System.out.println("教我游泳,交完後,教練回到了游泳池");
        return null;
    }
}
複製代碼

線程池練習:返回兩個數相加的結果

要求:經過線程池中的線程對象,使用Callable接口完成兩個數求和操做

  • Future接口:用來記錄線程任務執行完畢後產生的結果。線程池建立與使用
  • V get() 獲取Future對象中封裝的數據結果

代碼演示:

複製代碼
package thread;

import java.util.concurrent.*;

public class ThreadPoolDemo04 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
//建立線程池對象
        ExecutorService threadPool = Executors.newFixedThreadPool(2);

//建立一個Callable接口子類對象
//MyCallable c = new MyCallable();
        MyCallable c = new MyCallable(100, 200);
        MyCallable c2 = new MyCallable(10, 20);

//獲取線程池中的線程,調用Callable接口子類對象中的call()方法, 完成求和操做
//<Integer> Future<Integer> submit(Callable<Integer> task)
// Future 結果對象
        Future<Integer> result = threadPool.submit(c);
//此 Future 的 get 方法所返回的結果類型
        Integer sum = result.get();
        System.out.println("sum=" + sum);

//再演示
        result = threadPool.submit(c2);
        sum = result.get();
        System.out.println("sum=" + sum);
//關閉線程池(能夠不關閉)

    }
}

//Callable接口實現類
class MyCallable implements Callable<Integer> {
    //成員變量
    int x = 5;
    int y = 3;

    //構造方法
    public MyCallable() {
    }

    public MyCallable(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public Integer call() throws Exception {
        return x + y;
    }
}
複製代碼

線程安全

若是有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。

咱們經過一個案例,演示線程的安全問題:

電影院要賣票,咱們模擬電影院的賣票過程。假設要播放的電影是 「功夫熊貓3」,本次電影的座位共100個(本場電影只能賣100張票)。

咱們來模擬電影院的售票窗口,實現多個窗口同時賣 「功夫熊貓3」這場電影票(多個窗口一塊兒賣這100張票)

須要窗口,採用線程對象來模擬;須要票,Runnable接口子類來模擬

複製代碼
package thread;

//測試類
public class ThreadDemo05 {
    public static void main(String[] args) {
        //建立票對象
        Ticket ticket = new Ticket();
        //建立3個窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
//模擬票
 class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    @Override
    public void run() {
        //模擬賣票
        while(true){
            if (ticket > 0) {
                //模擬選坐的操做
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
        }
    }
}
複製代碼

運行結果發現:上面程序出現了問題

  • 票出現了重複的票
  • 錯誤的票 0、-1

其實,線程安全問題都是由全局變量及靜態變量引發的。若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做,通常都須要考慮線程同步,不然的話就可能影響線程安全。

線程同步(線程安全處理Synchronized)

java中提供了線程同步機制,它可以解決上述的線程安全問題。

線程同步的方式有兩種:

  • 方式1:同步代碼塊
  • 方式2:同步方法

同步代碼塊

同步代碼塊: 在代碼塊聲明上 加上synchronized

synchronized (鎖對象) {
    可能會產生線程安全問題的代碼
}

同步代碼塊中的鎖對象能夠是任意的對象;但多個線程時,要使用同一個鎖對象纔可以保證線程安全。

使用同步代碼塊,對電影院賣票案例中Ticket類進行以下代碼修改:

複製代碼
package thread;

//測試類
public class ThreadDemo05 {
    public static void main(String[] args) {
        //建立票對象
        Ticket ticket = new Ticket();
        //建立3個窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
//模擬票
 class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定義鎖對象
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步代碼塊
            synchronized (lock){
                if (ticket > 0) {
                    //模擬電影選坐的操做
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            }
        }
    }
}
複製代碼

當使用了同步代碼塊後,上述的線程的安全問題,解決了。

同步方法

同步方法:在方法聲明上加上synchronized

public synchronized void method(){
       可能會產生線程安全問題的代碼
}

同步方法中的鎖對象是 this

使用同步方法,對電影院賣票案例中Ticket類進行以下代碼修改:

複製代碼
package thread;

//測試類
public class ThreadDemo05 {
    public static void main(String[] args) {
        //建立票對象
        Ticket ticket = new Ticket();
        //建立3個窗口
        Thread t1  = new Thread(ticket, "窗口1");
        Thread t2  = new Thread(ticket, "窗口2");
        Thread t3  = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
//模擬票
 class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定義鎖對象
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步方法
            method();
        }
    }
//同步方法,鎖對象this
    public synchronized void method(){
        if (ticket > 0) {
            //模擬選坐的操做
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
        }
    }
}
複製代碼

靜態同步方法: 在方法聲明上加上static synchronized

public static synchronized void method(){
    可能會產生線程安全問題的代碼
}

靜態同步方法中的鎖對象是 類名.class

死鎖

同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,若是同步中嵌套了其餘的同步。這時容易引起一種現象:程序出現無限等待,這種現象咱們稱爲死鎖。這種狀況能避免就避免掉。

synchronzied(A鎖){
    synchronized(B鎖){
    }
}

咱們進行下死鎖狀況的代碼演示:

複製代碼
package thread;

import java.util.Random;

//定義鎖對象類
 class MyLock {
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();
}
//線程任務類
 class ThreadTask implements Runnable {
    int x = new Random().nextInt(1);//0,1
    //指定線程要執行的任務代碼
    @Override
    public void run() {
        while(true){
            if (x%2 ==0) {
                //狀況一
                synchronized (MyLock.lockA) {
                    System.out.println("if-LockA");
                    synchronized (MyLock.lockB) {
                        System.out.println("if-LockB");
                        System.out.println("if大口吃肉");
                    }
                }
            } else {
                //狀況二
                synchronized (MyLock.lockB) {
                    System.out.println("else-LockB");
                    synchronized (MyLock.lockA) {
                        System.out.println("else-LockA");
                        System.out.println("else大口吃肉");
                    }
                }
            }
            x++;
        }
    }
}

//測試類
public class ThreadDemo06 {
    public static void main(String[] args) {
        //建立線程任務類對象
        ThreadTask task = new ThreadTask();
        //建立兩個線程
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        //啓動線程
        t1.start();
        t2.start();
    }
}
複製代碼

Lock接口

查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可得到的更普遍的鎖定操做。

Lock接口中的經常使用方法

Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操做鎖的功能。

咱們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行以下代碼修改:

複製代碼
 class Ticket implements Runnable {
    //共100票
    int ticket = 100;
//建立Lock鎖對象
    Lock ck = new ReentrantLock();
    @Override
    public void run() {
//模擬賣票
        while (true) {
//synchronized (lock){
            ck.lock();
            if (ticket > 0) {
//模擬選坐的操做
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
            ck.unlock();
//}
        }
    }
}
複製代碼

等待喚醒機制

在開始講解等待喚醒機制以前,有必要搞清一個概念——線程之間的通訊:多個線程在處理同一個資源,可是處理的動做(線程的任務)卻不相同。經過必定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。

等待喚醒機制所涉及到的方法:

  • wait() :等待,將正在執行的線程釋放其執行資格 和 執行權,並存儲到線程池中。
  • notify():喚醒,喚醒線程池中被wait()的線程,一次喚醒一個,並且是任意的。
  • notifyAll(): 喚醒所有:能夠將線程池中的全部wait() 線程都喚醒。

其實,所謂喚醒的意思就是讓 線程池中的線程具有執行資格。必須注意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才能夠明確出這些方法操做的究竟是哪一個鎖上的線程。

仔細查看JavaAPI以後,發現這些方法 並不定義在 Thread中,也沒定義在Runnable接口中,卻被定義在了Object類中,爲何這些操做線程的方法定義在Object類中?

由於這些方法在使用時,必需要標明所屬的鎖,而鎖又能夠是任意對象。能被任意對象調用的方法必定定義在Object類中。

 

接下里,咱們先從一個簡單的示例入手:

 

如上圖說示,輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務是:

  • 1.當input發現Resource中沒有數據時,開始輸入,輸入完成後,叫output來輸出。若是發現有數據,就wait();
  • 2.當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,而後,叫醒input來輸入數據。

 下面代碼,模擬等待喚醒機制的實現:

複製代碼
package thread;

//模擬資源類
class Resource {
    private String name;
    private String sex;
    private boolean flag = false;

    public synchronized void set(String name, String sex) {
        if (flag)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
// 設置成員變量
        this.name = name;
        this.sex = sex;
// 設置以後,Resource中有值,將標記該爲 true ,
        flag = true;
// 喚醒output
        this.notify();
    }

    public synchronized void out() {
        if (!flag)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
// 輸出線程將數據輸出
        System.out.println("姓名: " + name + ",性別: " + sex);
// 改變標記,以便輸入線程輸入數據
        flag = false;
// 喚醒input,進行數據輸入
        this.notify();
    }
}


//輸入線程任務類
class Input implements Runnable {
    private Resource r;

    public Input(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        int count = 0;
        while (true) {
            if (count == 0) {
                r.set("小明", "男生");
            } else {
                r.set("小花", "女生");
            }
// 在兩個數據之間進行切換
            count = (count + 1) % 2;
        }
    }
}


//輸出線程任務類
class Output implements Runnable {
    private Resource r;

    public Output(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.out();
        }
    }
}

//測試類
public class ThreadDemo07 {
    public static void main(String[] args) {
// 資源對象
        Resource r = new Resource();
// 任務對象
        Input in = new Input(r);
        Output out = new Output(r);
// 線程對象
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
// 開啓線程
        t1.start();
        t2.start();
    }
}
相關文章
相關標籤/搜索