關於synchronized、wait、notify已經notifyAll的使用

  前言:關於synchronized、wait、notify已經notifyAll大家應該不陌生,現在我大致說一下我的理解。

一:synchronized

synchronized中文解釋是同步,那麼什麼是同步呢,解釋就是程序中用於控制不同線程間操作發生相對順序的機制,通俗來講就是2點,第一要有多線程,第二當多個線程同時競爭某個資源的時候會有先後順序。在java中有三種寫synchronized的方式

  • 第一種:
    • 寫在普通方法的前面,這種表示對實例對象加鎖。
  • 第二種:
    • 寫在靜態方法前面,這種表示對類對象加鎖
  • 第三種:
    • 寫在代碼塊中,鎖是Synchonized括號裏配置的對象(可能是實例對象,也可能是類對象)

總體說來就2種,一種就是鎖實例對象,一種鎖類對象。

鎖實例對象就是當多個線程同時操作這個實例對象的時候必須先獲取鎖,如果無法獲取鎖,則必須處於等待狀態,而和鎖類對象區別是,當多個線程同時操作的時候,任何以這個類對象實例化的對象都要獲取鎖才能操作。舉個簡單例子

比如一個羣人去打飯,只要是人就必須排隊等待,一個個的打飯。不管是誰,但是吃完飯之後把盤子送回原地,但是這個時候不同的人可能吃飯快慢不同,但是肯定先吃飯後送盤子。現在寫段代碼我們比對一下。

複製代碼

public class RunnableTest implements Runnable {

    private synchronized  void testSyncMethod() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getId() + "testSyncMethod:" + i);
        }
    }

    public void run() {
     testSyncMethod();
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(2);
        RunnableTest rt = new RunnableTest();
        RunnableTest rt1 = new RunnableTest();
        exec.execute(rt);
        exec.execute(rt1);
        exec.shutdown();
    }

複製代碼

按照我們的理論輸出結果肯定是無序排列的。如圖

複製代碼

public class RunnableTest implements Runnable {
    private void testSyncBlock() {
        synchronized (RunnableTest.class) {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getId()+"testSyncBlock:" + i);
            }
        }
    }
    public void run() {
     testSyncBlock();
    }
    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(2);
        RunnableTest rt = new RunnableTest();
        RunnableTest rt1 = new RunnableTest();
        exec.execute(rt);
        exec.execute(rt1);
        exec.shutdown();
    }
}

複製代碼

而這段代碼輸入結果肯定是有序的。如下

那麼我們在思考一個問題,如果類A有2個方法,如果我們在其中一個方法前面加入了synchronized,哪意味着我們別的線程調用這個類的另一個方法也需要獲取鎖纔可以執行,也是另一個方法只是讀,這樣一來性能就大大的降低,所以我們在實際開發中儘量少在方法前加入synchronized,那麼我們應該怎麼做呢,既然是實際對象我們只需要加入一個類,鎖定此類,只需要讓類的一個方法進行鎖定即可。ok下面代碼如下

複製代碼

public class A {
    private Object obj="123";
    public  void a(){
        synchronized (obj) {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread() + "a:" + i);
            }
        }
    }
    public void b(){
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread() + "b:" + i);
            }

    }
}
public class B implements Runnable{
    private A a;
    public B(A a){
        this.a=a;
    }

    public void run() {
        a.b();
    }
}
public class C implements Runnable {
    private A a;
    public C(A a){
        this.a=a;
    }
    public void run() {
        a.a();
    }
}
public class E implements Runnable{
    private A a;
    public E(A a){
        this.a=a;
    }
    public void run() {
        a.a();
    }
}
public class D {
    public static void main(String[] args) {
        A a=new A();
        ExecutorService executorService= Executors.newCachedThreadPool();
        executorService.execute(new E((a)));
        executorService.execute(new B(a));
        executorService.execute(new C(a));
        executorService.shutdown();
    }
}

複製代碼

按照我們理論這段代碼執行順序是第一個線程和第二個線程無序,第三個線程必須等待第一個線程執行完畢纔可以,測試結果也論證了我們的理論如下

二:wait、notify已經notifyAll

wait、notify、notifyAll是Object對象的屬性,並不屬於線程。我們先解釋這三個的一個很重要的概念

wait:使持有該對象的線程把該對象的控制權交出去,然後處於等待狀態(這句話很重要,也就是說當調用wait的時候會釋放鎖並處於等待的狀態)

notify:通知某個正在等待這個對象的控制權的線程可以繼續運行(這個就是獲取鎖,使自己的程序開始執行,最後通過notify同樣去釋放鎖,並喚醒正在等待的線程)

notifyAll:會通知所有等待這個對象控制權的線程繼續運行(和上面一樣,只不過是喚醒所有等待的線程繼續執行)

這個就好了,從上面的解釋我們可以看出通過wait和notify可以做線程之間的通信,當A線程處理完畢通知B線程執行,B線程執行完畢以後A線程可以繼續執行。ok我們使用例子來說明。

 

複製代碼

public class Temp {
    int count=0;
    public void waiter() throws InterruptedException {
        synchronized (this) {
               System.out.println("等待");
               wait();
               System.out.println(this.count);
        }
    }
    public void notifyer() throws InterruptedException {
        synchronized (this){
            TimeUnit.SECONDS.sleep(1);
            System.out.println("喚醒");
            for (int i=0;i<10;i++){
                System.out.println(Thread.currentThread()+"notifyer:"+i);
                count+=i;
            }
            notify();
        }
    }

public class Waiter implements Runnable{
    private Temp temp;
    public Waiter(Temp temp){
        this.temp=temp;
    }
    public void run() {
        try {
            temp.waiter();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Notifyer implements Runnable{
    private Temp temp;
    public Notifyer(Temp temp){
        this.temp=temp;
    }
    public void run() {
        try {
            temp.notifyer();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

   public static void main(String[] args) {
        Temp temp=new Temp();
        ExecutorService executorService= Executors.newCachedThreadPool();
        executorService.execute(new Waiter(temp));
        executorService.execute(new Notifyer(temp));
        executorService.shutdown();
    }

複製代碼

其中在notify中加入休眠1s目的是讓線程waiter先執行更能看明白

我們在舉一個例子,比如說我們經常提到的客戶端請求和服務器響應,當客戶端發送請求後就處於等待服務端的響應,而服務端會等待客戶端端請求然後響應客戶端請求,下面我們看看怎麼去寫代碼

首先我們寫一個對象Handler來專門處理客戶端和服務端的,對於客戶端有2個方法就是發送請求和等待服務端響應,對於服務端同樣2個方法那就是等待客戶端請求和響應客戶端

複製代碼

public class Handler {
    private boolean isClientRequest=false;
    public void sendRequest(){
        synchronized (this){
            isClientRequest=true;
            this.notifyAll();
        }
    }
    public void waitResponse() throws InterruptedException {
        synchronized (this){
            while (isClientRequest){
                this.wait();
            }
        }
    }

    public void receiveRequest(){
        synchronized (this) {
            isClientRequest = false;
            this.notifyAll();
        }
    }
    public void waitRequest() throws InterruptedException {
        synchronized (this){
            while (!isClientRequest){
                this.wait();
            }
        }
    }
}

複製代碼

現在我們寫客戶端代碼,客戶端肯定先發送請求,但是先等待1s爲了讓服務端處於等待的效果,發送請求後就處於等待狀態直到服務端的響應

複製代碼

public class Client implements Runnable {
    private Handler handler;

    public Client(Handler handler) {
        this.handler = handler;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                System.out.println("客戶端發送請求");
                TimeUnit.SECONDS.sleep(1);
                this.handler.sendRequest();//第二步
                System.out.println("等待服務端的響應");
                this.handler.waitResponse();//第三步
            }
        } catch (InterruptedException e) {

        }
        System.out.println("客戶端已經完成請求");
    }

複製代碼

然後我們寫服務端代碼,服務端首先處於等待狀態,收到客戶端請求後立馬進行處理,處理完畢之後再次等待客戶端的請求

複製代碼

public class Server implements Runnable {
    public Handler handler;

    public Server(Handler handler) {
        this.handler = handler;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                System.out.println("等待客戶端請求");
                this.handler.waitRequest();//第一步
                System.out.println("處理客戶端請求");
                TimeUnit.SECONDS.sleep(1);
                this.handler.receiveRequest();//第四步
            }
        } catch (InterruptedException e) {

        }
        System.out.println("服務端處理已經完成");
    }
}

複製代碼

從上面我們預測肯定是等待客戶端請求,發送請求,等待響應,處理客戶端請求這樣循環的結果。如下

在說一下wait和sleep的區別

區別1:在wait期間對象鎖使釋放的

區別2:可以通過notify和notifyAll,或者玲命令到期,從wait中恢復執行。如果wait不接受任何參數,這種wait將無線的等待下去,直到線程收到notify或notifyall的消息

1、Sleep(long)是Thread的方法,而wait()/wait(long)是Object的方法

    2、Sleep(long)可以放在sychnoized塊內也可以不放在裏面,但是wait()/wait(long)必須放在語句塊內

    3、Sleep(long)不釋放鎖,只是讓當前線程暫停一段時間,而wait()/wait(long)是釋放鎖

    4、wait()將當前線程放到阻塞隊列,只有調用notify()/notifyAll()方法後,纔將其從阻塞隊列中移動到就緒隊列,等待被CPU調度,而wait(long)方法執行後就是放到阻塞隊列,等待時間到期或者被wait()/wait(long)喚醒後就可以放到就緒隊列被CPU調度

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