JUC并發(fā)工具類詳細(xì)使用教程與案例解析

juc并發(fā)工具類解決了傳統(tǒng)并發(fā)編程中synchronized和wait()/notify()的粒度粗、靈活性差、易出錯等問題,1.提供reentrantlock實(shí)現(xiàn)更細(xì)粒度的鎖控制,支持trylock、lockinterruptibly等特性;2.通過executorservice線程池高效管理線程資源,降低創(chuàng)建銷毀開銷;3.使用atomic系列原子類實(shí)現(xiàn)無鎖線程安全操作;4.利用countdownlatch、cyclicbarrier、semaphore等同步器協(xié)調(diào)復(fù)雜線程協(xié)作;5.引入condition替代wait/notify,支持多條件隊(duì)列;6.提供concurrenthashmap等高性能并發(fā)集合減少鎖競爭,相比傳統(tǒng)機(jī)制在高并發(fā)場景下具備更高的靈活性與性能。

JUC并發(fā)工具類詳細(xì)使用教程與案例解析

JUC并發(fā)工具類是Java多線程編程領(lǐng)域提供的一系列強(qiáng)大組件,它們極大地簡化了復(fù)雜并發(fā)場景的開發(fā),幫助我們更安全、高效地管理線程協(xié)作與數(shù)據(jù)共享,有效避免了傳統(tǒng)同步機(jī)制的諸多限制和陷阱。理解并恰當(dāng)運(yùn)用這些工具,是構(gòu)建高性能、健壯并發(fā)應(yīng)用的關(guān)鍵所在。

JUC并發(fā)工具類詳細(xì)使用教程與案例解析

解決方案

JUC并發(fā)工具類詳細(xì)使用教程與案例解析

要深入掌握J(rèn)UC并發(fā)工具類,核心在于理解其設(shè)計(jì)哲學(xué)——將并發(fā)編程中常見的模式和問題抽象化為易于使用的類庫。這包括:通過顯式鎖(如ReentrantLock)提供比synchronized更細(xì)粒度的控制和更豐富的功能;利用線程池(ExecutorService)高效管理線程資源,避免頻繁創(chuàng)建銷毀的開銷;使用原子類(Atomic系列)實(shí)現(xiàn)無鎖的線程安全操作;以及通過各種同步器(如CountDownLatch、CyclicBarrier、Semaphore)協(xié)調(diào)線程間的復(fù)雜協(xié)作。實(shí)際應(yīng)用中,關(guān)鍵是根據(jù)具體的業(yè)務(wù)需求和并發(fā)模式,選擇最合適的JUC工具,而不是一概而論。

為什么我們需要JUC,它解決了哪些傳統(tǒng)并發(fā)編程的痛點(diǎn)?

JUC并發(fā)工具類詳細(xì)使用教程與案例解析

說實(shí)話,剛開始接觸Java并發(fā),我們最先學(xué)到的多半是synchronized關(guān)鍵字和wait()/notify()。它們確實(shí)能解決一些基本的同步問題,但在面對更復(fù)雜的場景時(shí),就顯得有些力不從心了。比如,synchronized是塊級別的鎖,一旦進(jìn)入同步塊,整個對象就被鎖住了,粒度比較粗,在高并發(fā)下可能導(dǎo)致性能瓶頸。它還不支持嘗試獲取鎖(tryLock)、中斷等待鎖(lockInterruptibly),以及超時(shí)獲取鎖這些靈活的操作。

再看wait()和notify(),它們的用法也挺講究,必須配合synchronized使用,而且容易出現(xiàn)“虛假喚醒”(Spurious Wakeups)的問題,或者因?yàn)橥沶otify()而導(dǎo)致線程永久等待。更頭疼的是,如果需要多個條件來喚醒不同的線程組,wait()/notify()就顯得非常笨拙,你可能得寫一大條件判斷。

JUC的出現(xiàn),正是為了解決這些痛點(diǎn)。它提供了一套更高級、更靈活、性能更好的并發(fā)工具。例如,ReentrantLock提供了非阻塞獲取鎖、可中斷獲取鎖、以及超時(shí)獲取鎖的能力,這在處理死鎖或提高響應(yīng)性方面非常有用。而Condition對象則完美替代了wait()/notify(),它允許一個鎖關(guān)聯(lián)多個條件隊(duì)列,極大地簡化了復(fù)雜條件下的線程協(xié)作。此外,JUC還引入了非阻塞算法(通過Atomic類實(shí)現(xiàn)),以及高效的并發(fā)集合類(如ConcurrentHashMap),它們在很多場景下比傳統(tǒng)的Collections.synchronizedMap性能要好得多,因?yàn)樗鼈儨p少了鎖的競爭。

ReentrantLock與Synchronized:我該如何選擇與應(yīng)用?

這是個老生常談的問題,但確實(shí)是理解JUC繞不開的一環(huán)。簡單來說,synchronized是Java語言層面的關(guān)鍵字,由jvm隱式管理鎖的獲取和釋放,用起來非常簡潔,不容易出錯(比如忘記釋放鎖)。它的性能在JDK后續(xù)版本中也得到了大幅優(yōu)化,在低競爭或中等競爭的場景下,性能可能不比ReentrantLock差多少,甚至更好。JVM會對其進(jìn)行偏向鎖、輕量級鎖等優(yōu)化。

public class SynchronizedExample {     private int count = 0;      public synchronized void increment() {         count++;     }      public int getCount() {         return count;     } }

而ReentrantLock是JUC包下的一個類,它提供了更細(xì)粒度的控制。最大的區(qū)別在于,ReentrantLock需要我們手動調(diào)用lock()和unlock()方法來獲取和釋放鎖。這意味著你必須非常小心地在finally塊中釋放鎖,否則一旦代碼拋出異常,鎖就永遠(yuǎn)不會被釋放,這會直接導(dǎo)致死鎖。

import java.util.concurrent.locks.ReentrantLock;  public class ReentrantLockExample {     private final ReentrantLock lock = new ReentrantLock();     private int count = 0;      public void increment() {         lock.lock(); // 獲取鎖         try {             count++;         } finally {             lock.unlock(); // 確保在finally塊中釋放鎖         }     }      public int getCount() {         return count;     }      public void tryIncrement() {         if (lock.tryLock()) { // 嘗試獲取鎖,不阻塞             try {                 count++;                 System.out.println(Thread.currentThread().getName() + " incremented count.");             } finally {                 lock.unlock();             }         } else {             System.out.println(Thread.currentThread().getName() + " failed to acquire lock.");         }     } }

那么,何時(shí)選擇ReentrantLock呢?

  1. 需要非阻塞地獲取鎖:tryLock()方法可以在不阻塞當(dāng)前線程的情況下嘗試獲取鎖。
  2. 需要可中斷地獲取鎖:lockInterruptibly()方法允許在等待鎖的過程中響應(yīng)中斷。
  3. 需要多個條件變量:ReentrantLock可以通過newCondition()創(chuàng)建多個Condition對象,實(shí)現(xiàn)更復(fù)雜的線程間協(xié)作。
  4. 需要公平鎖:ReentrantLock可以構(gòu)造為公平鎖(new ReentrantLock(true)),它會嘗試將鎖授予等待時(shí)間最長的線程,而synchronized是非公平的。

如果你只是需要一個簡單的互斥鎖,并且對鎖的控制粒度沒有特殊要求,那么synchronized通常是更簡潔、更安全的選擇。如果你需要更高級的鎖特性,或者在某些高競爭場景下需要極致的性能調(diào)優(yōu),那么ReentrantLock會是更好的工具。

ExecutorService線程池:構(gòu)建高效并發(fā)應(yīng)用的核心利器

直接管理線程(即每次任務(wù)來就new Thread())是低效且危險(xiǎn)的。線程的創(chuàng)建和銷毀都有不小的開銷,而且無限制地創(chuàng)建線程可能耗盡系統(tǒng)資源,導(dǎo)致OOM(OutOfMemoryError)甚至系統(tǒng)崩潰。ExecutorService正是為了解決這些問題而生的。它提供了一個框架,用于管理和復(fù)用線程,將任務(wù)的提交與任務(wù)的執(zhí)行解耦。

使用線程池的好處顯而易見:

  • 降低資源消耗:通過復(fù)用已存在的線程,減少線程創(chuàng)建和銷毀的開銷。
  • 提高響應(yīng)速度:任務(wù)到達(dá)時(shí),可以直接從池中獲取線程執(zhí)行,無需等待線程創(chuàng)建。
  • 提高可管理性:可以對線程池進(jìn)行統(tǒng)一的調(diào)優(yōu)、監(jiān)控和管理,比如限制并發(fā)線程的數(shù)量。

ExecutorService是java.util.concurrent.Executor接口的擴(kuò)展,提供了生命周期管理和提交Callable和Runnable任務(wù)的方法。通常,我們會通過Executors工廠類來創(chuàng)建不同類型的線程池:

  • newFixedThreadPool(int nThreads):創(chuàng)建固定大小的線程池。當(dāng)提交的任務(wù)多于線程數(shù)時(shí),多余的任務(wù)會在隊(duì)列中等待。
  • newCachedThreadPool():創(chuàng)建一個可緩存的線程池。如果線程池中有空閑線程,則復(fù)用;如果沒有,則創(chuàng)建新線程。空閑線程在一定時(shí)間后會被回收。
  • newSingleThreadExecutor():創(chuàng)建一個單線程的線程池。它會保證所有任務(wù)都按照提交順序執(zhí)行。
  • newScheduledThreadPool(int corePoolSize):創(chuàng)建一個支持定時(shí)及周期性任務(wù)執(zhí)行的線程池。

當(dāng)然,更高級的用法是直接構(gòu)造ThreadPoolExecutor,這樣可以完全自定義線程池的各項(xiàng)參數(shù),包括核心線程數(shù)、最大線程數(shù)、空閑線程存活時(shí)間、任務(wù)隊(duì)列、線程工廠以及拒絕策略等。這在生產(chǎn)環(huán)境中進(jìn)行性能調(diào)優(yōu)時(shí)非常關(guān)鍵。

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit;  public class ExecutorServiceExample {      public static void main(String[] args) {         // 創(chuàng)建一個固定大小為3的線程池         ExecutorService executor = Executors.newFixedThreadPool(3);          for (int i = 0; i < 10; i++) {             final int taskId = i;             executor.submit(() -> {                 System.out.println("Executing task " + taskId + " by " + Thread.currentThread().getName());                 try {                     Thread.sleep(1000); // 模擬任務(wù)執(zhí)行時(shí)間                 } catch (InterruptedException e) {                     Thread.currentThread().interrupt();                     System.out.println("Task " + taskId + " was interrupted.");                 }             });         }          // 關(guān)閉線程池,不再接受新任務(wù),但會執(zhí)行已提交的任務(wù)         executor.shutdown();         try {             // 等待所有任務(wù)執(zhí)行完畢,最多等待10秒             if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {                 System.err.println("Pool did not terminate in time. Forcibly shutting down.");                 executor.shutdownNow(); // 立即關(guān)閉,嘗試中斷正在執(zhí)行的任務(wù)             }         } catch (InterruptedException e) {             executor.shutdownNow();             Thread.currentThread().interrupt();         }         System.out.println("All tasks submitted and pool is shutting down.");     } }

在使用線程池時(shí),一個常見的陷阱就是忘記調(diào)用shutdown()方法。如果不調(diào)用,線程池中的線程會一直保持活躍狀態(tài),導(dǎo)致程序無法正常退出。另一個需要注意的,是選擇合適的任務(wù)隊(duì)列和拒絕策略,這直接影響到線程池在高負(fù)載下的行為。比如,如果使用無界隊(duì)列(如LinkedBlockingQueue),即使maximumPoolSize設(shè)置得再小,也可能因?yàn)槿蝿?wù)堆積而導(dǎo)致內(nèi)存溢出。

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊9 分享