Java JDK 中提供的線程通信方式

JDK 中提供的線程通信方式

1、wait/notify方式

   wait/notify要求在同步關鍵字中使用,避免了死鎖現象,但是如果不先調用wait,而先調用notify的情況下,容易導致線程永久掛起
package com.milla.study.netbase.expert.concurrent;

import java.util.Objects;
import java.util.concurrent.locks.LockSupport;

/**
 * @Package: com.milla.study.netbase.expert.concurrent
 * @Description: <線程通信類>
 * @Author: MILLA
 * @CreateDate: 2020/4/21 14:29
 * @UpdateUser: MILLA
 * @UpdateDate: 2020/4/21 14:29
 * @UpdateRemark: <>
 * @Version: 1.0
 */
public class ThreadCommunicationTest {
    private Object product;

    public static void main(String[] args) throws InterruptedException {
        ThreadCommunicationTest test = new ThreadCommunicationTest();

        test.waitAndNotifyAll();//正常情況
        test.waitAndNotifyAllWaitingByOrder();//順序問題導致一直阻塞
     }

    private void waitAndNotifyAllWaitingByOrder() throws InterruptedException {
        Thread consumer = new Thread(() -> {

            if (Objects.isNull(product)) {
                System.out.println("沒有產品可以消費,進行等待...");
                try {
                    Thread.sleep(5000L);//外接因素導致線程先被喚醒後才進行等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (this) {
                    try {
                        this.wait();//該方法會執行後會失去鎖,被喚醒後需從該語句的後一個語句開始執行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("繼續消費...........");
            }
        });
        consumer.start();
        System.out.println("啓動消費者線程...");
        Thread.sleep(1000L);
        synchronized (this) {
            this.notifyAll();
            System.out.println("生產出產品,喚醒所有等待線程...");
        }
    }

    private void waitAndNotifyAll() throws InterruptedException {
        Thread consumer = new Thread(() -> {
            synchronized (this) {
                while (Objects.isNull(product)) {
                    System.out.println("沒有產品可以消費,進行等待...");
                    try {
                        this.wait();//被喚醒後需從該語句的後一個語句開始執行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("繼續消費...........");
            }
        });
        consumer.start();
        System.out.println("啓動消費者線程...");
        Thread.sleep(1000L);
        synchronized (this) {
//            product = new Object();//當生產者不被創建的時候時候,喚醒線程,線程會出現僞喚醒的情況。所以在使用wait的時候應採用while的判斷模式[JDK推薦方式],而不是if
            this.notifyAll();
            System.out.println("生產出產品,喚醒所有等待線程...");
        }
    }
}

執行後,如果不能成功運行結束,可以通過兩種方式去查看線程的狀態:

1、jstack命令

//在黑窗口中執行jps命令,找到java進程的Pid
jsp -l // 獲取所有的Java程序信息

23408 org/netbeans/Main
18132 org.jetbrains.jps.cmdline.Launcher
20804 sun.tools.jps.Jps
22692 org.jetbrains.jps.cmdline.Launcher
4712 com.milla.study.netbase.expert.concurrent.ThreadCommunicationTest//確定該進程pid
4728 org.jetbrains.jps.cmdline.Launcher
9016 org.jetbrains.idea.maven.server.RemoteMavenServer
6108

//繼續執行jstack -jdk自帶的命令

jstack -l 4712

在輸出中沒有找到dead lock的情況下,一般就是沒有死鎖,如果看到 waiting for lock的話,一般是因爲一個線程持有鎖,然後是在阻塞中,且不釋放鎖,而另一個線程也獲取不到這個鎖,就一直阻塞

2.通過JavaVisualVM [JDK中自帶的可視化工具]

查看線程的Dump,基本信息和jstack一樣

3、使用jconsole工具 [jdk提供的可視化工具]

指定需要監測的進程,可分本地和遠程進程

結果顯示,並沒有產生死鎖,只是線程一直處於阻塞狀態

2、通過park/unpark實現通信

       park/unpark沒有順序要求,但park並不會釋放鎖,在同步代碼快中依然有永久阻塞的問題

package com.milla.study.netbase.expert.concurrent;

import java.util.Objects;
import java.util.concurrent.locks.LockSupport;

/**
 * @Package: com.milla.study.netbase.expert.concurrent
 * @Description: <線程通信類>
 * @Author: MILLA
 * @CreateDate: 2020/4/21 14:29
 * @UpdateUser: MILLA
 * @UpdateDate: 2020/4/21 14:29
 * @UpdateRemark: <>
 * @Version: 1.0
 */
public class ThreadCommunicationTest {
    private Object product;

    public static void main(String[] args) throws InterruptedException {
        ThreadCommunicationTest test = new ThreadCommunicationTest();
 
        test.parkAndUnPark();//正常情況
        test.parkAndUnParkWaitingBySynchronized();//順序問題導致一直阻塞
    }

    private void parkAndUnParkWaitingBySynchronized() throws InterruptedException {
        Thread consumer = new Thread(() -> {
            System.out.println("開始消費....");
            while (Objects.isNull(product)) {
                System.out.println("沒有許可,阻塞中....");
                synchronized (this) {//同步代碼塊,park方法不會釋放鎖,所以,一直在阻塞狀態
                    LockSupport.park();//許可是不可用的,將線程阻塞
                }
            }
            System.out.println("繼續消費....");
        });
        consumer.start();
        Thread.sleep(3000L);
        product = new Object();

        synchronized (this) {
            LockSupport.unpark(consumer);
        }
        System.out.println("發放許可,使線程繼續執行....");
    }

    private void parkAndUnPark() throws InterruptedException {
        Thread consumer = new Thread(() -> {
            System.out.println("開始消費....");
            while (Objects.isNull(product)) {
                System.out.println("沒有許可,阻塞中....");
                LockSupport.park();//許可是不可用的,將線程阻塞
            }
            System.out.println("繼續消費....");
        });
        consumer.start();
        Thread.sleep(3000L);
        product = new Object();

        LockSupport.unpark(consumer);//發送許可,使線程能繼續執行
        System.out.println("發放許可,使線程繼續執行....");
    }
}

3、已經廢棄的suspend/resume

        對調用有順序要求,開發中容易出現死鎖現象,也容易永久掛起,已經被廢棄

package com.milla.study.netbase.expert.concurrent;

import java.util.Objects;
import java.util.concurrent.locks.LockSupport;

/**
 * @Package: com.milla.study.netbase.expert.concurrent
 * @Description: <線程通信類>
 * @Author: MILLA
 * @CreateDate: 2020/4/21 14:29
 * @UpdateUser: MILLA
 * @UpdateDate: 2020/4/21 14:29
 * @UpdateRemark: <>
 * @Version: 1.0
 */
public class ThreadCommunicationTest {
    private Object product;

    public static void main(String[] args) throws InterruptedException {
        ThreadCommunicationTest test = new ThreadCommunicationTest();

        test.oldJdkCommunication();//正常情況
        test.oldJdkCommunicationWaitingBySynchronized();//因爲同步塊導致永久阻塞
        test.oldJdkCommunicationWaitingByOrder();//喚醒順序問題導致永久掛起
    }


    //方法調用順序錯誤導致死鎖
    private void oldJdkCommunicationWaitingByOrder() throws InterruptedException {
        Thread producer = new Thread(() -> {

            if (Objects.isNull(product)) {
                System.out.println("如果產品是空的話,掛起當前線程[需要獲取到鎖],進入等等");
                synchronized (this) {
                    try {
                        Thread.sleep(3000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Thread.currentThread().suspend();//掛起線程
                }
            }
            System.out.println("繼續消費");
        }, "生產者");

        producer.start();
        Thread.sleep(1000L);
        product = new Object();
        producer.resume();//恢復消費線程,在線程被掛起之前喚醒線程,線程掛起之後就一直處於阻塞狀態
        System.out.println("重新喚起線程進行消費");
        Thread.sleep(3001L);
        producer.resume();//繼續喚醒,此時如果在線程被掛起之後繼續喚醒線程,還是可以喚醒成功的()
        System.out.println("繼續喚醒...");
    }

    //同步代碼塊會死鎖
    private void oldJdkCommunicationWaitingBySynchronized() throws InterruptedException {
        Thread producer = new Thread(() -> {

            if (Objects.isNull(product)) {
                System.out.println("如果產品是空的話,掛起當前線程[需要獲取到鎖],進入等等");
                synchronized (this) {
                    Thread.currentThread().suspend();//掛起線程
                }
            }
            System.out.println("繼續消費");
        }, "生產者");

        producer.start();
        Thread.sleep(3000L);
        product = new Object();
        synchronized (this) {
            producer.resume();//恢復消費線程
        }
        System.out.println("重新喚起線程進行消費");
    }

    private void oldJdkCommunication() throws InterruptedException {
        Thread producer = new Thread(() -> {

            if (Objects.isNull(product)) {
                System.out.println("如果產品是空的話,掛起當前線程[需要獲取到鎖],進入等等");
                synchronized (this) {
                    Thread.currentThread().suspend();//掛起線程
                }
            }
            System.out.println("繼續消費");
        }, "生產者");

        producer.start();
        Thread.sleep(3000L);
        product = new Object();
        producer.resume();//恢復消費線程
        System.out.println("重新喚起線程進行消費");

    }
}

注意點:在判斷的時候JDK中推薦採用while(conditions){wait()}.的方式,不建議直接使用if判斷,因爲線程可能不是notify、notifyAll、 unpark等api的調用而喚醒的,可能是更底層的原因喚醒的,應注意僞喚醒的問題。

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