JUC之執行緒間的通訊

xbhog發表於2021-12-29

執行緒通訊

對上次多執行緒程式設計步驟補充(中部):

  1. 建立資源類,在資源類中建立屬性和操作方法
  2. 在資源類裡面操作
    • 判斷
    • 幹活
    • 通知
  3. 建立多個執行緒,呼叫資源類的操作方法

執行緒通訊的實現例子:

兩個執行緒,實現對一個初始變數為0進行操作,一個執行緒對其+1,一個執行緒對其-1,使得變數結果不改變

使用Synchronized實現的執行緒通訊:

package com.JUC;

/**
 * 建立資源類
 */
class Share{
    //初始值
    private int number = 0;

    //建立方法
    public synchronized void incr() throws InterruptedException {
        //判斷 幹活 通知
        if(number != 0){
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"::"+number);
        //通知其他執行緒
        this.notifyAll();
        //System.out.println(this.getClass());
    }
    public synchronized void decr() throws InterruptedException {
        if(number != 1){
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"::"+number);
        //喚醒其他的執行緒,這裡的this指代在方法中指呼叫該方法的物件
        this.notifyAll();
    }

}
public class ThreadSignaling {
    public static void main(String[] args) throws InterruptedException {
        Share share = new Share();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AAA").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BBB").start();
    }
}

volatile和synchronized關鍵字

volatile:即可見性,當修改一個變數的時候,如果該變數是通過volatile修飾的,那麼其他所有的執行緒都會感知到該變數的變化情況。

如果不使用該關鍵字的話:

可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。

舉個簡單的例子,看下面這段程式碼:

//執行緒1執行的程式碼
int i = 0;
i = 10;

//執行緒2執行的程式碼
j = i;

 假若執行執行緒1的是CPU1,執行執行緒2的是CPU2。由上面的分析可知,當執行緒1執行 i =10這句時,會先把i的初始值載入到CPU1的快取記憶體中,然後賦值為10,那麼在CPU1的快取記憶體當中i的值變為10了,卻沒有立即寫入到主存當中。

  此時執行緒2執行 j = i,它會先去主存讀取i的值並載入到CPU2的快取當中,注意此時記憶體當中i的值還是0,那麼就會使得j的值為0,而不是10.

  這就是可見性問題,執行緒1對變數i修改了之後,執行緒2沒有立即看到執行緒1修改的值

上述的解釋其實可以對應到書中的以下片段:

Java支援多個執行緒同時訪問一個物件或者物件的成員變數,由於每個執行緒可以擁有這個變數的拷貝(雖然物件以及成員變數分配的記憶體是在共享記憶體中的,但是每個執行的執行緒還是可以擁有一份拷貝,這樣做的目的是加速程式的執行,這是現代多核處理器的一個顯著特性),所以程式在執行過程中,一個執行緒看到的變數並不一定是最新的。

使用關鍵字synchronized可以修飾方法或者同步塊;

作用:確保多個執行緒在同一時刻,只能由一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性。

任何一個物件都有其對應的監視器,當這個物件由同步塊或者同步方法呼叫的時候,需要進行以下邏輯:

image-20211218204458960

任意執行緒對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取。

Condition的使用

與synchronized再做一個比較:

Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法wait notify和notifyAll的使用;

使用Lock condition介面實現買票:

package com.JUC;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class shareDemo {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private int number = 0;

    public void inc() throws InterruptedException {
        lock.lock();

        try{
            while(number != 0){
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"::"+number);
            /**
             * 喚醒多有等待的執行緒
             */
            condition.signalAll();

        }finally {
            lock.unlock();
        }
    }
    public void sub(){
        lock.lock();

        try{
            while(number != 1){
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"::"+number);
            /**
             * 喚醒多有等待的執行緒
             */
            condition.signalAll();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class ConditionLocal {
    public static void main(String[] args) {
        shareDemo share = new shareDemo();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.inc();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        },"AAA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                share.sub();
            }

        },"BBB").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.inc();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        },"CCC").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                share.sub();
            }

        },"DDD").start();
    }
}

在書籍4.3.1-4.3.3對應的其實是該文章中執行緒通訊的例子。

管道輸入/輸出流:

執行緒間通訊的方式還有管道輸入/輸出流:與檔案的輸入輸出不同的是,它主要用於執行緒間的資料傳輸,傳輸的媒介是記憶體;

以下是書中的內容:

管道輸入/輸出流主要包括瞭如下4種具體實現:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前兩種面向位元組,而後兩種面向字元。

實現例子:

對於Piped型別的流,必須先要進行繫結,也就是呼叫connect()方法,如果沒有將輸入/輸

出流繫結起來,對於該流的訪問將會丟擲異常

package com.JUC;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

public class PipeInOut {
    public static void main(String[] args) throws IOException {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        //將輸入流和輸出流進行連線,否則會出現IO錯誤
        out.connect(in);
        //建立print執行緒來接收Main中的輸入
        Thread thread = new Thread(new Print(in),"PrintThread");
        //開啟該執行緒,開始接收資料
        thread.start();
        int receive = 0;
        try {
            //接收輸入的資料並賦值
            while((receive = System.in.read()) != -1){
                out.write(receive);
            }
        }finally{
            out.close();
        }

    }

    static class Print implements Runnable {
        private  PipedReader in;
        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while((receive = in.read()) != -1){
                    System.out.print((char) receive);
                }
            }catch(IOException ex){

            }
        }
    }
}

image-20211229193144160

Thread.join()的使用

書中的定義:其含義是:當前執行緒A等待thread執行緒終止之後才從thread.join()返回;感覺不太好理解;

Java 7 Concurrency Cookbook

是主執行緒等待子執行緒的終止。也就是說主執行緒的程式碼塊中,如果碰到了t.join()方法,此時主執行緒需要等待(阻塞),等待子執行緒結束了(Waits for this thread to die.),才能繼續執行t.join()之後的程式碼塊。

例子:

package com.JUC;

import java.util.concurrent.TimeUnit;

public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread previous = Thread.currentThread();
        for (int i = 0; i < 10; i++) {
            // 每個執行緒擁有前一個執行緒的引用,需要等待前一個執行緒終止,才能從等待中返回
            Thread thread = new Thread(new Domino(previous), String.valueOf(i));
            thread.start();
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5);
        //主執行緒
        System.out.println(Thread.currentThread().getName()+"--terminate.");
    }

    static class Domino implements Runnable {
        private Thread thread;
        public Domino(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //子執行緒
            System.out.println(Thread.currentThread().getName()+"---Terminate.");
        }
    }
}

每個執行緒終止的前提是前驅執行緒的終止,每個執行緒等待前驅執行緒終止後,才從join()方法返回;

我們檢視下join方法的原始碼可以發現其中也是用的synchronized修飾的;

public final void join() throws InterruptedException {
    join(0);
}
public final synchronized void join(long millis)
    throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

關於書中的4.3.6ThreadLocal的使用可以看下以前寫的文章:點選進入

參考:

《JUC併發程式設計的藝術》

《【尚矽谷】大廠必備技術之JUC併發程式設計》

相關文章