Java多執行緒知識點

  • 2020 年 3 月 27 日
  • 筆記

基礎概念

進程和執行緒的區別?多執行緒有什麼好處? 進程:正在進行中的程式(直譯)。 執行緒:就是進程中一個負責程式執行的控制單元(執行路徑)

  • 一個進程中可以多執行路徑,稱之為多執行緒,一個進程中至少要有一個執行緒。
  • 開啟多個執行緒是為了同時運行多部分程式碼。 每一個執行緒都有自己運行的內容。這個內容可以稱為執行緒要執行的任務。
  • 多執行緒好處:解決了多部分同時運行的問題。
  • 什麼時候使用多執行緒?當需要多部分程式碼同時執行的時候,可以使用。

編寫多執行緒程式有幾種實現方式? 一種是繼承Thread類;另一種是實現Runnable介面。兩種方式都要通過重寫run()方法來定義執行緒的行為,推薦使用後者,因為Java中的繼承是單繼承,一個類有一個父類,如果繼承了Thread類就無法再繼承其他類了,顯然使用Runnable介面更為靈活。Runnable不是執行緒,是執行緒里運行的程式碼

class Demo implements Runnable// extends Fu    //準備擴展Demo類的功能,讓其中的內容可以作為執行緒的任務執行  {      public void run() {          show();      }      public void show() {          for (int x = 0; x < 20; x++) {              System.out.println(Thread.currentThread().getName() + "....." + x);          }      }  }  class ThreadDemo {      public static void main(String[] args) {          Demo d = new Demo();          Thread t1 = new Thread(d);          Thread t2 = new Thread(d);          t1.start();          t2.start();      }  }

Java並發Concurrent包——Callable/Future/FutureTask解析

Callable 介面類似於 Runnable,兩者都是為那些其實例可能被另一個執行緒執行的類設計的。但是 Runnable 不會返回結果,並且無法拋出經過檢查的異常。 省了自己寫回調了

public interface Callable<V> {  // 計算結果,如果無法計算結果,則拋出一個異常  V call() throws Exception;  }

FutureTask、RunnableFuture相比runnable有結果的返回

    public static class CountTask implements Callable<Integer>{            @Override          public Integer call() throws Exception {              System.out.println("子執行緒開始計算");              Thread.sleep(5000);              System.out.println("子執行緒結束計算,共用時5秒");              return 100;          }        }    ExecutorService executor = Executors.newCachedThreadPool();          // Future的使用          //Future<Integer> result = executor.submit(new CountTask());          // FutureTask的使用          FutureTask<Integer> futureTask = new FutureTask<>(new CountTask());          executor.submit(futureTask);          executor.shutdown();          // Future獲取結果          //Integer i = result.get();          // futureTask獲取結果          Integer i = futureTask.get();

ThreadLocal 原理分析

  • 多個執行緒都有一個資源,這個資源可以放在ThreadLocal里,在一個執行緒中修改不影響其他執行緒的使用。每個執行緒都有這個資源。
  • 簡單說ThreadLocal就是一種以空間換時間的做法,在每個Thread裡面維護了一個ThreadLocalMap,而這個map的key就是當前執行緒,值就是我們set的那個值,每次執行緒在get的時候,都從自己的變數中取值,既然從自己的變數中取值,所以不會影響其他執行緒。在這個執行緒是獨享的,也沒有執行緒安全方面的問題。

執行緒的基本狀態以及狀態之間的關係? 創建並運行執行緒:

  • 新建狀態(New Thread):在Java語言中使用new 操作符創建一個執行緒後,該執行緒僅僅是一個空對象,它具備類執行緒的一些特徵,但此時系統沒有為其分配資源,這時的執行緒處於創建狀態。執行緒處於創建狀態時,可通過Thread類的方法來設置各種屬性,如執行緒的優先順序(setPriority)、執行緒名(setName)和執行緒的類型(setDaemon)等。
  • 就緒狀態(Runnable):使用start()方法啟動一個執行緒後,系統為該執行緒分配了除CPU外的所需資源,使該執行緒處於就緒狀態。此外,如果某個執行緒執行了yield()方法,那麼該執行緒會被暫時剝奪CPU資源,重新進入就緒狀態。
  • 運行狀態(Running):Java運行系統通過調度選中一個處於就緒狀態的執行緒,使其佔有CPU並轉為運行狀態。此時,系統真正執行執行緒的run()方法。

可以通過Thread類的isAlive方法來判斷執行緒是否處於就緒/運行狀態:當執行緒處於就緒/運行狀態時,isAlive返回true,當isAlive返回false時,可能執行緒處於阻塞狀態,也可能處於停止狀態。

  • 阻塞和喚醒執行緒阻塞狀態(Blocked):一個正在運行的執行緒因某些原因不能繼續運行時,就進入阻塞 狀態。這些原因包括: a) 當執行了某個執行緒對象的sleep()等阻塞類型的方法時,該執行緒對象會被置入一個阻塞集內,等待超時而自動蘇醒。 b) 當多個執行緒試圖進入某個同步區域時,沒能進入該同步區域的執行緒會被置入鎖定集,直到獲得該同步區域的鎖,進入就緒狀態。 c) 當執行緒執行了某個對象的wait()方法時,執行緒會被置入該對象的等待集中,知道執行了該對象的notify()方法wait()/notify()方法的執行要求執行緒首先獲得該對象的鎖。
  • 死亡狀態(Dead):執行緒在run()方法執行結束後進入死亡狀態。此外,如果執行緒執行了interrupt()或stop()方法,那麼它也會以異常退出的方式進入死亡狀態。

join join(插隊):一種特殊的wait,當前運行執行緒調用另一個執行緒的join方法,當前執行緒進入阻塞狀態直到另一個執行緒運行結束等待該執行緒終止。 注意該方法也需要捕捉異常。

執行緒的sleep()方法和yield()方法有什麼區別?

  1. sleep()方法給其他執行緒運行機會時不考慮執行緒的優先順序,因此可能會給低優先順序的執行緒以運行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以運行的機會; Thread.setPriority(Thread.MAX_PRIORITY);
  2. 執行緒執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(Runnable)狀態。
  3. yield() 使得執行緒放棄當前分得的 CPU 時間,但是不使執行緒阻塞,即執行緒仍處於可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果等價於調度程式認為該執行緒已執行了足夠的時間從而轉到另一個執行緒。
  4. sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常

wait 和 sleep 區別? sleep來自Thread類,和wait來自Object類 調用sleep()方法的過程中,執行緒不會釋放對象鎖。而 調用 wait 方法執行緒會釋放對象鎖 sleep(milliseconds)需要指定一個睡眠時間,時間一到會自動喚醒

wait和notify wait:使一個執行緒處於等待(阻塞/凍結)狀態,並且釋放所持有的對象的鎖,讓其他執行緒可以進入Synchronized數據塊,當前執行緒被放入對象等待池中; notify():喚醒一個處於等待狀態的執行緒,當然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的執行緒,而是由JVM確定喚醒哪個執行緒,而且與優先順序無關; notityAll():喚醒所有處於等待狀態的執行緒,該方法並不是將對象的鎖給所有執行緒,而是讓它們競爭,只有獲得鎖的執行緒才能進入就緒狀態; 用法 在未達到目標時 wait() 用 while 循環檢查 設置完成後 notifyAll() wait() 和 notify() / notifyAll() 都需要放在同步程式碼塊里

為什麼stop()方法被廢棄而不被使用呢? 原因是stop()方法太過於暴力,會強行把執行一半的執行緒終止。這樣會就不會保證執行緒的資源正確釋放,通常是沒有給與執行緒完成資源釋放工作的機會,因此會導致程式工作在不確定的狀態下。 使用boolean類型的變數,來終止執行緒 或者使用interrupt

啟動一個執行緒是調用run()還是start()方法? 啟動一個執行緒是調用start()方法,使執行緒所代表的虛擬處理機處於可運行狀態,這意味著它可以由JVM 調度並執行,這並不意味著執行緒就會立即運行。run()方法是執行緒啟動後要進行回調(callback)的方法。

一個執行緒如果出現了運行時異常會怎麼樣 如果這個異常沒有被捕獲的話,這個執行緒就停止執行了。另外重要的一點是:如果這個執行緒持有某個某個對象的監視器(鎖),那麼這個對象監視器會被立即釋放

守護進程 t1.start(); t2.setDaemon(true); // setDameon是守護執行緒,可以理解為後台執行緒,你停我也停。前台必須手動結束 t2.start();

多次start一個執行緒會怎麼樣 會拋出java.lang.IllegalThreadStateException 執行緒狀態非法異常


執行緒安全問題

執行緒安全問題的本質 在多個執行緒訪問共同的資源時,在某⼀個執行緒對資源進行寫操作的中途(寫入已經開始,但還沒結束),其他執行緒對這個寫了一半的資源進行行了讀操作,或者基於這個寫了一半的資源進行了寫操作,導致出現數據錯誤。 鎖機制的本質 通過對共享資源進行訪問限制,讓同一時間只有一個執行緒可以訪問資源,保證了數據的準確性。

不論是執行緒安全問題,還是針對執行緒安全問題所衍生出的鎖機制,它們的核心都在於共享的資源,而不是某個方法或者某幾行程式碼。

解決思路 就是將多條操作共享數據的執行緒程式碼封裝起來,當有執行緒在執行這些程式碼的時候,其他執行緒時不可以參與運算。必須要當前執行緒把這些程式碼都執行完畢後,其他執行緒才可以參與運算。 使用鎖機制:synchronized 或 lock 對象 同步的好處:解決了執行緒的安全問題。 同步的弊端:相對降低了效率,因為同步外的執行緒的都會判斷同步鎖。 同步的前提:同步中必須有多個執行緒並使用同一個鎖。

當一個執行緒進入一個對象的一個synchronized方法後,其它執行緒是否可進入此對象的其它方法? 不能,一個對象的一個synchronized方法只能由一個執行緒訪問。

同步函數和同步程式碼塊的區別 同步函數的鎖是固定的this。同步程式碼塊的鎖是任意的對象。建議使用同步程式碼塊。 靜態方法的同步函數的鎖是class類,不是this對象,靜態方法不屬於某個對象,多個類是共享的 private final Object monitor1 = new Object(); 一般用object當鎖就行了

簡述synchronized 和Lock的異同?

  • jdk1.5以後將同步和鎖封裝成了對象。並將操作鎖的隱式方式定義到了該對象中,將隱式動作變成了顯示動作。
  • synchronized會自動釋放鎖,而Lock一定要求程式設計師手工釋放,並且最好在finally 塊中釋放(這是釋放外部資源的最好的地方)。
  • Lock介面: 出現替代了同步程式碼塊或者同步函數。將同步的隱式鎖操作變成現實鎖操作。同時更為靈活。可以一個鎖上加上多組監視器。
  • lock():獲取鎖。unlock():釋放鎖,通常需要定義finally程式碼塊中。

讀寫鎖

finally 的作用:保證在方法提前結束或出現 Exception 的時候,依然能正常釋放鎖。

一般並不會只是使用 Lock ,而是會使用更複雜的鎖,例如ReadWriteLock

public class ReadWriteLockDemo implements TestDemo {    private int x = 0;      ReentrantReadWriteLock lock = new ReentrantReadWriteLock();    Lock readLock = lock.readLock();    Lock writeLock = lock.writeLock();      private void count() {      writeLock.lock();      try {        x++;      } finally {        writeLock.unlock();      }    }      private void print(int time) {      readLock.lock();      try {        System.out.print(x + " ");      } finally {        readLock.unlock();      }    }      @Override    public void runTest() {    }  }

悲觀鎖

  • 總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個執行緒使用,其它執行緒阻塞,用完後再把資源轉讓給其它執行緒)。
  • 傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。
  • 樂觀鎖,總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖。更多的時候用在資料庫里(經常改變,先判斷,如果需要的話再鎖,悲觀鎖是直接就鎖))。

Java記憶體模型 Java記憶體模型定義了一種多執行緒訪問Java記憶體的規範。 Java記憶體模型將記憶體分為了主記憶體和工作記憶體。類的狀態,也就是類之間共享的變數,是存儲在主記憶體中的,每次Java執行緒用到這些主記憶體中的變數的時候,會讀一次主記憶體中的變數,並讓這些記憶體在自己的工作記憶體中有一份拷貝,運行自己執行緒程式碼的時候,用到這些變數,操作的都是自己工作記憶體中的那一份。在執行緒程式碼執行完畢之後,會將最新的值更新到主記憶體中去

原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。 有序性:即程式執行的順序按照程式碼的先後順序執行。 可見性:指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值

volatile關鍵字的作用 volatile關鍵字的作用主要有兩個:

  1. 使用volatile關鍵字修飾的變數,保證了其在多執行緒之間的可見性(有序性,看2)

當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。保證了每次讀取到volatile變數,一定是最新的數據。

  1. 程式碼底層執行不像我們看到的高級語言,Java程式這麼簡單,它的執行是Java程式碼–>位元組碼–>根據位元組碼執行對應的C/C++程式碼–>C/C++程式碼被編譯成彙編語言–>和硬體電路交互,現實中,為了獲取更好的性能JVM可能會對指令進行重排序,多執行緒下可能會出現一些安全的問題。使用volatile則會對禁止語義重排序,當然這也一定程度上降低了程式碼執行效率 從實踐角度而言,volatile的一個重要作用就是和CAS結合,保證了原子性,詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger( /əˈtɑːmɪk/ )、 AtomicBoolean 等類,作用和 volatile 基本一致,可以看做是通用版的 volatile
AtomicInteger atomicInteger = new AtomicInteger(0);  ...  atomicInteger.getAndIncrement();//++count

volatile為什麼不能保證原子性 一個變數i被volatile修飾,兩個執行緒想對這個變數修改,都對其進行自增操作也就是i++,i++的過程可以分為三步,首先獲取i的值,其次對i的值進行加1,最後將得到的新值寫會到快取中。 執行緒A首先得到了i的初始值100,但是還沒來得及修改,就阻塞了,這時執行緒B開始了,它也得到了i的值,由於i的值未被修改,即使是被volatile修飾,主存的變數還沒變化,那麼執行緒B得到的值也是100,之後對其進行加1操作,得到101後,將新值寫入到快取中,再刷入主存中。根據可見性的原則,這個主存的值可以被其他執行緒可見。 問題來了,執行緒A已經讀取到了i的值為100,也就是說讀取的這個原子操作已經結束了,所以這個可見性來的有點晚,執行緒A阻塞結束後,繼續將100這個值加1,得到101,再將值寫到快取,最後刷入主存,所以即便是volatile具有可見性,也不能保證對它修飾的變數具有原子性。

Synchronized和Volatile的比較

  1. Synchronized保證記憶體可見性和操作的原子性 ,保證不了有序性
  2. Volatile只能保證記憶體可見性,有序性,保證不了原子性
  3. Volatile不需要加鎖,比Synchronized更輕量級,並不會阻塞執行緒(volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞。)
  4. volatile標記的變數不會被編譯器優化,而synchronized標記的變數可以被編譯器優化(如編譯器重排序的優化).
  5. volatile是變數修飾符,僅能用於變數,而synchronized是一個方法或塊的修飾符。
  6. volatile本質是在告訴JVM當前變數在暫存器中的值是不確定的,使用前,需要先從主存中讀取,因此可以實現可見性。而對n=n+1,n++等操作時,volatile關鍵字將失效,不能起到像synchronized一樣的執行緒同步(原子性)的效果。

執行緒池

執行緒池的好處

  • 因為不需要每次處理複雜邏輯耗時操作都創建一個執行緒,比如載入網路,避免了執行緒的創建和銷毀所帶來的性能開銷和消耗的時間,能有效控制執行緒池的最大並發數,避免了大量執行緒間搶佔資源而導致的阻塞現象
  • 能夠對執行緒進行簡單的管理,並提供定時執行以及指定間隔循環執行等功能(ExecutorService 是安全的)
  • 避免頻繁地創建和銷毀執行緒,達到執行緒對象的重用。另外,使用執行緒池還可以根據項目靈活地控制並發的數目。

執行緒池相關方法

  • Java為我們提供了 ExecutorService 執行緒池來優化和管理執行緒的使用。
  • Executors類是官方提供的一個工廠類,它裡面封裝好了眾多功能不一樣的執行緒池,他們的內部其實是通過:ThreadPoolExecutor
  • 既然執行緒池就是ThreadPoolExecutor,所以我們要創建一個執行緒池只需要new ThreadPoolExecutor(…);就可以創建一個執行緒池
public ThreadPoolExecutor(int corePoolSize,                                int maximumPoolSize,                                long keepAliveTime,                                TimeUnit unit,                                BlockingQueue<Runnable> workQueue,                                ThreadFactory threadFactory,                                RejectedExecutionHandler handler) {...}

執行緒池的參數

  1. 核心執行緒數(最多同時運行幾個),當執行緒池中的執行緒數目達到corePoolSize後,就會把到達的任務放到快取隊列當中;默認是0,需要自己去實現。
  2. 表示在執行緒池中最多能創建多少個執行緒;
  3. 如果執行緒池沒有要執行的任務 存活多久
  4. 存活時間的單位
  5. 如果 執行緒池裡管理的執行緒都已經用了,剩下的任務 臨時存到LinkedBlockingQueue對象中排隊,先進先出
  6. threadFactory 執行緒工廠,如何去創建執行緒的,可以自定義執行緒創建的執行者,他們有適當的執行緒名稱、優先順序,甚至他們還可以守護進程。

LinkedBlockingQueue 在Java多執行緒應用中,隊列的使用率很高,多數生產消費模型的首選數據結構就是隊列(先進先出)。Java提供的執行緒安全的Queue可以分為阻塞隊列(安全)和非阻塞隊列,其中阻塞隊列的典型例子是BlockingQueue,非阻塞隊列的典型例子ConcurrentLinkedQueue(也是安全的,cas)。

LinkedBlockingQueue 是執行緒安全的隊列,通過ReentrantLock保證的。(鏈表實現的隊列) 由於LinkedBlockingQueue實現是執行緒安全的,實現了先進先出等特性,是作為生產者消費者的首選,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的話,默認最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在隊列滿的時候會阻塞直到有隊列成員被消費,take方法在隊列空的時候會阻塞,直到有隊列成員被放進來。 2 的 31 次方 – 1

Java自己的執行緒池

  • newSingleThreadExecutor:創建一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒串列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。
  • newFixedThreadPool:創建固定大小的執行緒池。每次提交一個任務就創建一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。
  • newCachedThreadPool:創建一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閑(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的添加新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠創建的最大執行緒大小。
  • newScheduledThreadPool:創建一個大小無限的執行緒池。此執行緒池支援定時以及周期性執行任務的需求。 。
  • 如果希望在伺服器上使用執行緒池,強烈建議使用newFixedThreadPool方法來創建執行緒池,這樣能獲得更好的性能。

自定義執行緒池ThreadPoolExecutor 自定義ThreadManager類管理多執行緒,例如

  • 請求網路數據執行緒交由長時間任務執行緒池執行
  • 訪問資料庫交由短時間任務執行緒池執行
  • 圖片下載任務將由單任務執行緒池執行
  • 20個圖片都做一些操作,創建固定執行緒池

開啟執行緒數一般是cpu的核數* 2+1

Executors弊端 Executors的4個功能執行緒池雖然方便,但現在已經不建議使用了,而是建議直接通過使用ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確執行緒池的運行規則,規避資源耗盡的風險。 Executors的4個功能執行緒有如下弊端: FixedThreadPool和SingleThreadExecutor:主要問題是堆積的請求處理隊列均採用LinkedBlockingQueue,可能會耗費非常大的記憶體,甚至OOM。 CachedThreadPool和ScheduledThreadPool:主要問題是執行緒數最大數是Integer.MAX_VALUE,可能會創建數量非常多的執行緒,甚至OOM。

newFixedThreadPool 創建一個固定執行緒數量的執行緒池

public static ExecutorService newFixedThreadPool(int nThreads) {      return new ThreadPoolExecutor(nThreads, nThreads,                                    0L, TimeUnit.MILLISECONDS,                                    new LinkedBlockingQueue<Runnable>());  }

特點:只有核心執行緒,執行緒數量固定,執行完立即回收,任務隊列為鏈表結構的有界隊列。 應用場景:控制執行緒最大並發數。

newSingleThreadExecutor 創建一個只有一個執行緒的執行緒池,每次只能執行一個執行緒任務,多餘的任務會保存到一個任務隊列中,等待執行緒處理完再依次處理任務隊列中的任務

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {      return new FinalizableDelegatedExecutorService          (new ThreadPoolExecutor(1, 1,                                  0L, TimeUnit.MILLISECONDS,                                  new LinkedBlockingQueue<Runnable>(),                                  threadFactory));  }

特點:只有1個核心執行緒,無非核心執行緒,執行完立即回收,任務隊列為鏈表結構的有界隊列。 應用場景:不適合併發但可能引起IO阻塞性及影響UI執行緒響應的操作,如資料庫操作、文件操作等。

newCachedThreadPool 創建一個可以根據實際情況調整執行緒池中執行緒的數量的執行緒池

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                    60L, TimeUnit.SECONDS,                                    new SynchronousQueue<Runnable>(),                                    threadFactory);  }

特點:無核心執行緒,非核心執行緒數量無限,執行完閑置60s後回收,任務隊列為不存儲元素的阻塞隊列。 應用場景:執行大量、耗時少的任務。 newScheduledThreadPool 創建一個可以定時或者周期性執行任務的執行緒池

public ScheduledThreadPoolExecutor(int corePoolSize) {      super(corePoolSize, Integer.MAX_VALUE,            DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,            new DelayedWorkQueue());  }

特點:核心執行緒數量固定,非核心執行緒數量無限,執行完閑置10ms後回收,任務隊列為延時阻塞隊列。 應用場景:執行定時或周期性的任務。


生產者消費者

多個執行緒在處理同一資源,但是任務卻不同。 生產者和消費者在同一時間段內共用同一個存儲空間,生產者向空間里存放數據,而消費者取用數據,如果不加以協調可能會出現以下情況: 存儲空間已滿,而生產者佔用著它,消費者等著生產者讓出空間從而去除產品,生產者等著消費者消費產品,從而向空間中添加產品。互相等待,從而發生死鎖。

wait()和notify()方法的實現 緩衝區滿和為空時都調用wait()方法等待,當生產者生產了一個產品或者消費者消費了一個產品之後會喚醒所有執行緒。

public class Test1 {      private static Integer count = 0;      private static final Integer FULL = 10;      private static String LOCK = "lock";        public static void main(String[] args) {          Test1 test1 = new Test1();          new Thread(test1.new Producer()).start();          new Thread(test1.new Consumer()).start();          new Thread(test1.new Producer()).start();          new Thread(test1.new Consumer()).start();          new Thread(test1.new Producer()).start();          new Thread(test1.new Consumer()).start();      }      class Producer implements Runnable {          @Override          public void run() {              for (int i = 0; i < 10; i++) {                  try {                      Thread.sleep(3000);                  } catch (Exception e) {                      e.printStackTrace();                  }                  synchronized (LOCK) {                      while (count == FULL) {                          try {                              LOCK.wait();                          } catch (Exception e) {                              e.printStackTrace();                          }                      }                      count++;                      System.out.println(Thread.currentThread().getName() + "生產者生產,目前總共有" + count);                      LOCK.notifyAll();                  }              }          }      }      class Consumer implements Runnable {          @Override          public void run() {              for (int i = 0; i < 10; i++) {                  try {                      Thread.sleep(3000);                  } catch (InterruptedException e) {                      e.printStackTrace();                  }                  synchronized (LOCK) {                      while (count == 0) {                          try {                              LOCK.wait();                          } catch (Exception e) {                          }                      }                      count--;                      System.out.println(Thread.currentThread().getName() + "消費者消費,目前總共有" + count);                      LOCK.notifyAll();                  }              }          }      }  }

死鎖

指兩個或兩個以上的執行緒在執行過程中,因爭奪資源而造成的一種互相等待的現象。就是多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。

public class DeadlockTest {        public static void main(String[] args) {          String str1 = new String("資源1");          String str2 = new String("資源2");            new Thread(new Lock(str1, str2), "執行緒1").start();          new Thread(new Lock(str2, str1), "執行緒2").start();      }  }    class Lock implements Runnable {        private String str1;      private String str2;        public Lock(String str1, String str2) {          super();          this.str1 = str1;          this.str2 = str2;      }        @Override      public void run() {          try {              System.out.println(Thread.currentThread().getName() + "運行");              synchronized (str1) {                  System.out.println(Thread.currentThread().getName() + "鎖住"+ str1);                  Thread.sleep(1000);                  synchronized (str2) {                      // 執行不到這裡                      System.out.println(Thread.currentThread().getName()                              + "鎖住" + str2);                  }              }          } catch (Exception e) {              e.printStackTrace();          }      }  }    執行緒1運行  執行緒1鎖住資源1  執行緒2運行  執行緒2鎖住資源2

執行緒1運行執行緒1鎖住資源1,執行緒2運行執行緒2鎖住資源2,兩個執行緒是同時執行的,執行緒1鎖住了資源1,執行緒2鎖住了資源2,執行緒1企圖鎖住資源2,但是資源2已經被執行緒2鎖住了,執行緒2企圖鎖住資源1,但是資源1已經被執行緒1鎖住了,然後就死鎖了。 你的同步(鎖)有我的同步,我的同步有你同步

要出現死鎖問題需要滿足以下條件

  1. 互斥條件:一個資源每次只能被一個執行緒使用。
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:進程已獲得的資源,在未使用完之前,不能強行剝奪。
  4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

解決方法

  1. 如果想要打破互斥條件,我們需要允許進程同時訪問某些資源,這種方法受制於實際場景,不太容易實現條件;
  2. 打破不可搶佔條件,這樣需要允許進程強行從佔有者那裡奪取某些資源,或者簡單一點理解,佔有資源的進程不能再申請佔有其他資源,必須釋放手上的資源之後才能發起申請,這個其實也很難找到適用場景;
  3. 進程在運行前申請得到所有的資源,否則該進程不能進入準備執行狀態。這個方法看似有點用處,但是它的缺點是可能導致資源利用率和進程並發性降低;
  4. 避免出現資源申請環路,即對資源事先分類編號,按號分配。這種方式可以有效提高資源的利用率和系統吞吐量,但是增加了系統開銷,增大了進程對資源的佔用時間。