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. 避免出現資源申請環路,即對資源事先分類編號,按號分配。這種方式可以有效提高資源的利用率和系統吞吐量,但是增加了系統開銷,增大了進程對資源的佔用時間。