Java 併發學習筆記(二)

請參看前一篇文章:Java 併發學習筆記(一)——原子性、可見性、有序性問題java

6、等待—通知機制

什麼是等待通知—機制?當線程不知足某個條件,則進入等待狀態;若是線程知足要求的某個條件後,則通知等待的線程從新執行。編程

等待通知機制的流程通常是這樣的:線程首先獲取互斥鎖,當不知足某個條件的時候,釋放互斥鎖,並進入這個條件的等待隊列;一直等到知足了這個條件以後,通知等待的線程,而且須要從新獲取互斥鎖。segmentfault

1. 等待-通知機制的簡單實現

等待-通知機制可使用 Java 的 synchronized 關鍵字,配合 wait()、notify()、notifyAll() 這個三個方法來實現。安全

前面說到的解決死鎖問題的那個例子,一次性申請全部的資源,使用的是循環等待,這在併發量很大的時候比較消耗 CPU 資源。併發

如今使用等待-通知機制進行優化:app

final class Monitor {
    
    private List<Object> res = new ArrayList<>(2);
    
    /**
     * 一次性申請資源 
     */
    public synchronized void apply(Object resource1, Object resource2) {
        while (res.contains(resource1) || res.contains(resource2)){
            try {
                //條件不知足則進入等待隊列
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        res.add(resource1);
        res.add(resource2);
    }
    /** 
     * 歸還資源 
     */
    public synchronized void free(Object resource1, Object resource2){
        res.remove(resource1);
        res.remove(resource2);
        //釋放資源以後,通知等待的線程開始執行
        this.notifyAll();
    }
}
2. 須要注意的地方

1) 每一個互斥鎖都有相應的等待隊列,例如上面的例子,就存在兩個等待隊列,一是 synchronized 入口等待隊列,二是 while 循環這個條件的等待隊列。ide

2) 調用 wait() 方法,會使當前線程釋放持有的鎖,並進入這個條件的等待隊列。知足條件以後,隊列中的線程被喚醒,不是立刻執行,而是須要從新獲取互斥鎖。例如上圖中,if 條件的隊列中的線程被喚醒後,須要從新進入 synchronized 處獲取互斥鎖。學習

3. wait 和 sleep 的區別

相同點:兩個方法都會讓渡 CPU 的使用權,等待再次被調度。優化

不一樣點:this

  • wait 屬於 Object 的方法,sleep 是 Thread 的方法
  • wait 只能在同步方法或同步塊中調用,sleep 能夠在任何地方調用
  • wait 會釋放線程持有的鎖,sleep 不會釋放鎖資源

7、管程理論

1. 什麼是管程?

指的是對共享變量和對共享變量的操做的管理,使其支持併發,對應到 Java,指的是管理類的成員變量和方法,讓這個類是線程安全的。

2. 管程模型

管程主要的模型有 Hasen、Hoare、MESA ,其中 MESA 最經常使用。管程的 MESA 模型主要解決的是線程的互斥和同步問題,和上面說到的等待-通知機制十分相似。示意圖以下:

在這裏插入圖片描述

首先看看管程是如何實現互斥的?在管程的入口有一個等待隊列,一次只容許一個線程進入管程。每一個條件對應一個等待隊列,當線程不知足條件的時候,進入對應的等待隊列;當條件知足的時候,隊列中的線程被喚醒,從新進入到入口處的等待隊列獲取互斥鎖,這就實現了線程的同步問題。

3. 管程的最佳實踐

接下來使用代碼實現了一個簡單的阻塞隊列,這就是一個很典型的管程模型,解決了線程互斥和同步問題。

public class BlockingQueue<T> {
    private int capacity;
    private int size;

    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    /**
     * 入隊列
     */
    public void enqueue(T data){
        lock.lock();
        try {
            //若是隊列滿了,須要等待,直到隊列不滿
            while (size >= capacity){
                notFull.await();
            }
            //入隊代碼,省略
            //入隊以後,通知隊列已經不爲空了
            notEmpty.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 出隊列
     */
    public T dequeue(){
        lock.lock();
        try {
            //若是隊列爲空,須要等待,直到隊列不爲空
            while (size <= 0){
                notEmpty.await();
            }
            //出隊代碼,省略
            //出隊列以後,通知隊列已經不滿了
            notFull.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //實際應該返回出隊數據
        return null;
    }
}

8、Java 中的線程

1. 線程的生命週期

Java 中的線程共分爲了 6 種狀態,分別是:

  • NEW(初始化狀態)
  • RUNNABLE(可運行/運行狀態)
  • BLOCKED(阻塞狀態)
  • WAITING(無限時等待)
  • TIMED_WAITING(限時等待)
  • TERMINATED(終止狀態)
2. 線程狀態轉換
  • RUNNABLE 與 BLOCKED 狀態的轉換:在線程等待 synchronized 的鎖時,會進入 BLOCKED 狀態,當獲取到鎖以後,又轉換到 RUNNABLE 狀態。
  • RUNNABLE 與 WAITING 狀態的轉換:1) 線程獲取到 synchronized 鎖以後,而且調用了 wait() 方法。 2) 調用 Thread.join() 方法,例如線程 A 調用 join() 方法,線程 B 等待 A 執行完畢,等待期間 B 進入 WAITING 狀態,線程 A 執行完後,線程 B 切換到 RUNNABLE 狀態。3) 調用 LockSupport.park() 方法
  • RUNNABLE 與 TIMED_WAITING 狀態的轉換:以上三種狀況,分別在方法中加上超時參數便可。另外還有兩種狀況:Thread.sleep(long millis) 方法,LockSupprt.parkNanos(Object blocker, long deadline)。
  • NEW 到 RUNNABLE 狀態的轉換:在 Java 中新建立的線程,會當即進入 NEW 狀態,而後啓動線程進入 RUNNABLE 狀態。Java 中新建線程通常有三種方式:

    • 繼承 Thread 類

      public class MyThread extends Thread {
      
          @Override
          public void run() {
              System.out.println("I am roseduan");
          }
      
          public static void main(String[] args) {
              MyThread thread = new MyThread();
              thread.start();
          }
      }
    • 實現 Runnable 接口,並將其實現類傳給 Thread 做爲參數

      public class MyThread {
      
          public static void main(String[] args) {
              Thread thread = new Thread(new Print());
              thread.start();
          }
      }
      
      class Print implements Runnable{
          @Override
          public void run() {
              System.out.println("I am roseduan");
          }
      }
    • 實現 Collable 接口,將其實現類傳給線程池執行,而且能夠獲取返回結果

      public class ThreadTest {
      
          public static void main(String[] args) throws InterruptedException {
              //線程池
              BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
              ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1,
                      TimeUnit.HOURS, queue);
              //執行
              Future<?> submit = threadPool.submit(new Demo());
          }
      }
      
      class Demo implements Callable<String> {
      
          @Override
          public String call() {
              System.out.println("I am roseduan");
              return "I am roseduan";
          }
      }
  • NEW 到 TERMINATED 狀態的轉換:線程執行完 run() 方法後,會自動切換到 TERMINATED 狀態。若是手動停止線程,可使用 interrupt() 方法。
3. 局部變量的線程安全性

局部變量存在於方法中,每一個方法都有對應的調用棧幀,因爲每一個線程都有本身獨立的方法調用棧,所以局部變量並無被共享。因此即使多個線程同時調用同一個方法,方法內部的局部變量也是線程安全的,不須要單獨加鎖。

經極客時間《Java 併發編程實戰》專欄內容學習整理
相關文章
相關標籤/搜索