Java 多線程下篇 線程通訊

等待/通知機制

利用wait,notify實現的一個生產者、一個消費者和一個單位的緩存的簡單模型:

public class QueueBuffer {
    int n;
    boolean valueSet = false;

    synchronized int get() {
        if (!valueSet)
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        System.out.println("Got: " + n);
        valueSet = false;
        notify();
        return n;
    }

    synchronized void put(int n) {
        if (valueSet)
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        this.n = n;
        valueSet = true;
        System.out.println("Put: " + n);
        notify();
    }
}
public class Producer implements Runnable {
    
    private QueueBuffer q;

    Producer(QueueBuffer q) {
        this.q = q;
        new Thread(this, "Producer").start();
    }

    public void run() {
        int i = 0;
        while (true) {
            q.put(i++);
        }
    }

}
public class Consumer implements Runnable {
    
    private QueueBuffer q;

    Consumer(QueueBuffer q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }

    public void run() {
        while (true) {
            q.get();
        }
    }

}
public class Main {

    public static void main(String[] args) {
        QueueBuffer q = new QueueBuffer(); 
        new Producer(q); 
        new Consumer(q); 
        System.out.println("Press Control-C to stop."); 
    }

}

上面例子中, 我們生產了一個數據後就需要對這個數據進行消費. 如果生產了但數據沒有被獲取, 則生產線程會在等待中. 直到調用了 notify() 方法後纔會被繼續執行. 反之也是一樣的.

也就是說, wait() 方法是使線程暫停; notify() 方法是使線程繼續運行.

但是在使用時需要注意:

1.執行wait, notify時,不獲得鎖會如何?

public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        obj.wait();
        obj.notifyAll();
}

執行以上代碼, 會拋出java.lang.IllegalMonitorStateException的異常.

2.執行wait, notify時, 不獲得該對象的鎖會如何?

public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        Object lock = new Object();
        synchronized (lock) {
            obj.wait();
            obj.notifyAll();
        }
    }

執行代碼,同樣會拋出java.lang.IllegalMonitorStateException的異常

該對象的鎖 指的就是 obj 對象的鎖.

3.爲什麼在執行 wait, notify時, 必須獲得該對象的鎖

我們需要先知道 synchronized 的作用:

  • Java中每一個對象都可以成爲一個監視器(Monitor), 該Monitor由一個鎖(lock), 一個等待隊列(waiting queue), 一個入口隊列(entry queue).
  • 對於一個對象的方法, 如果沒有 synchronized 關鍵字, 該方法可以被任意數量的線程, 在任意時刻調用.
  • 對於添加了 synchronized 關鍵字的方法, 任意時刻只能被唯一的一個獲得了對象實例鎖的線程調用.
  • synchronized 用於實現多線程的同步操作.

當一個線程在執行 synchronized 的方法內部, 調用了 wait() 後, 該線程會釋放該對象的鎖, 然後該線程會被添加到該對象的等待隊列中(waiting queue), 只要該線程在等待隊列中, 就會一直處於閒置狀態, 不會被調度執行.

要注意 wait() 方法會強迫線程先進行釋放鎖操作, 所以在調用 wait() 時, 該線程必須已經獲得鎖, 否則會拋出異常(IllegalMonitorStateException). 由於 wait()synchonized 的方法內部被執行, 鎖一定已經獲得, 就不會拋出異常了.

當一個線程調用一個對象的 notify() 方法時, 調度器會從所有處於該對象等待隊列 (waiting queue) 的線程中取出任意一個線程, 將其添加到入口隊列 (entry queue) 中. 然後在入口隊列中的多個線程就會競爭對象的鎖, 得到鎖的線程就可以繼續執行. 如果等待隊列中(waiting queue)沒有線程, notify() 方法不會產生任何作用.

線程狀態

clipboard.png

  • NEW: 線程實例化時的默認狀態.
  • RUNNABLE: 一旦線程開始執行, 它就會移動到Runnable狀態. 請注意, 等待獲取 CPU 以供執行的線程仍處於此狀態.
  • BLOCKED: 線程一旦被阻塞, 就會等待監視器鎖, 並且移動到阻塞狀態. 有兩種方式可以進入阻塞狀態.

    • synchronised 同步代碼塊或同步方法.
    • 調用 Object.Wait 方法.
  • WAITING: 調用下列方法來將線程變爲等待狀態

    • Object.wait without a timeout
    • Thread.join without a timeout
    • LockSupport.park
  • TIMED_WAITING: 調用下列方法將線程變爲超時等待

    • Thread.sleep
    • Object.wait with a timeout
    • Thread.join with a timeout
    • LockSupport.parkNanos
    • LockSupport.parkUntil
  • TERMINATED: 一旦線程終止, 它就會移動到這種狀態.

通過管道進行線程通信: 字節流

用來讀取管道中的數據

public class ReadData extends Thread {

    private PipedInputStream pipedInputStream;

    public ReadData(PipedInputStream pipedInputStream) {
        this.pipedInputStream = pipedInputStream;
    }

    @Override
    public void run() {
        try {
            System.out.println("read :");
            byte[] byteArray = new byte[20];
            int readLen = this.pipedInputStream.read(byteArray);
            String newData = "";
            while(readLen != -1) {
                newData += new String(byteArray, 0, readLen);
                readLen = this.pipedInputStream.read(byteArray);

            }
            
            System.out.println(newData);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

用來給管道發送數據

public class WriteData extends Thread {

    private PipedOutputStream pipedOutputStream;

    public WriteData(PipedOutputStream pipedOutputStream) {
        this.pipedOutputStream = pipedOutputStream;
    }

    @Override
    public void run() {
        try {
            System.out.println("write :");
            for (int i = 0; i < 300; i++) {
                String outData = "" + (i + 1);
                this.pipedOutputStream.write(outData.getBytes());
                System.out.print(outData);
            }
            System.out.println();
            this.pipedOutputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
    public static void main(String[] args) throws IOException {

        PipedInputStream pipedInputStream = new PipedInputStream();
        PipedOutputStream pipedOutputStream = new PipedOutputStream();

        pipedOutputStream.connect(pipedInputStream);

        WriteData writeData = new WriteData(pipedOutputStream);
        ReadData readData = new ReadData(pipedInputStream);

        writeData.start();
        readData.start();

    }

pipedOutputStream.connect(pipedInputStream); 用來將兩個流之間產生通訊.

對於字節流和字符流是一樣的, 只需要使用 PipedWriterPipedReader.

join 方法使用

在一個線程(父線程)中創建另一個線程(子線程), 有些情況下, 我們需要等待子線程執行完成後, 父線程在繼續執行.

比如子線程處理一個數據, 父線程要取得這個數據中的值, 可以考慮使用 join 方法來實現.

不使用 join 方法前的問題

public class MyThread extends Thread {
    @Override
    public void run() {
        int i = (int) (Math.random() * 10000);
        System.out.println(i);
        try {
            Thread.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
    public static void main(String[] args) throws IOException {

        MyThread myThread = new MyThread();
        myThread.start();

        //Thread.sleep(?);

        System.out.println("我想當 myThread 執行完畢後再執行");
        System.out.println("但上面代碼中 sleep 中的值應該寫多少?");
        System.out.println("答案是: 值不能確定 :) ");
    }

使用 join 方法來解決問題

    public static void main(String[] args) throws IOException, InterruptedException {

        MyThread myThread = new MyThread();
        myThread.start();
        myThread.join();

        System.out.println("我想當 myThread 對象執行完畢後我再執行, 我做到了");
    }
join 與 synchronized 的區別是: join 在內部使用 wait 方法進行等待, 而 synchronized 關鍵字使用的是 "對象監視器" 原理做完同步.
並且如果遇到 interrupt 方法則會拋出, InterruptedException

join(long) 方法的使用

方法 join(long) 中的參數是設置等待的時間.

public class MyThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("執行完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public static void main(String[] args) throws IOException, InterruptedException {

    MyThread myThread = new MyThread();
    myThread.start();
    myThread.join(2000);

    System.out.println("等待2秒後執行");
}

從打印結果來看主線程只等待了兩秒後輸出了 "等待2秒後執行", 和 sleep 執行結果是一樣的. 主要原因還是來自於這2個方法同步的處理上.

方法 join(long) 與 sleep(long) 的區別, join(long) 會釋放鎖, sleep(long) 不會釋放鎖.

ThreadLocal 類的使用

變量值的共享可以使用 public static 變量的形式, 所有的線程都使用同一個 public static 變量. 如果想實現每一個線程都有自己的共享變量可以使用 ThreadLocal 類.

類 ThreadLocal 主要解決的就是每個線程綁定自己的值, 可以比喻成全局存放數據的盒子, 盒子中可以存儲每個線程的私有數據.

多個線程之間是隔離的.
public class Tools {
    public static ThreadLocal threadLocal = new ThreadLocal();
}
public class ThreadA extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 100; i++) {
                Tools.threadLocal.set("ThreadA" + (i + 1));
                System.out.println("ThreadA get Value=" + Tools.threadLocal.get());
                Thread.sleep(200);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadB extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 100; i++) {
                Tools.threadLocal.set("ThreadB" + (i + 1));
                System.out.println("ThreadB get Value=" + Tools.threadLocal.get());
                Thread.sleep(200);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

類 InheritableThreadLocal 的使用

使用 InheritableThreadLocal 類可以在子線程中取得父線程繼承下來的值.

值繼承

public class InheritableThreadLocalEx extends InheritableThreadLocal {
    @Override
    protected Object initialValue() {
        return new Date().getTime();
    }
}
public class Tools {
    public static InheritableThreadLocalEx inheritableThreadLocalEx = new InheritableThreadLocalEx();
}
public class ThreadA extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("ThreadA get Value=" + Tools.inheritableThreadLocalEx.get());
                Thread.sleep(100);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public static void main(String[] args) throws IOException, InterruptedException {

    for (int i = 0; i < 10; i++) {
        System.out.println("Main get Value=" + Tools.inheritableThreadLocalEx.get());
        Thread.sleep(100);
    }

    ThreadA threadA = new ThreadA();
    threadA.start();
}

值繼承再修改

public class InheritableThreadLocalEx extends InheritableThreadLocal {
    @Override
    protected Object initialValue() {
        return new Date().getTime();
    }

    @Override
    protected Object childValue(Object parentValue) {
        return parentValue + " 我在子線程加的~";
    }
}
注意, 如果子線程在取得值得同, 主線程將 InheritableThreadLocal 中的值進行更改, 那麼子線程取到的值還是就值.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章