JUC之線程間的通信

線程通信

對上次多線程編程步驟補充(中部):

  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併發編程》

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