【JavaEE】多線程進階(2)
- 一、JUC(java.util.concurrent) 的常?類
- 1.1 Callable 接?
- 1.2 ReentrantLock
- 1.3 原子類
- 原子類的特性:
- 常見原子類:
- 原子類的實例:
- 1.4 線程池
- 1.5 信號量 Semaphore
- 代碼實例
- 1.6 CountDownLatch
- 代碼實例
- 1.7 線程安全的集合類
- 多線程環境使? ArrayList
- 多線程環境使?哈希表
- Hashtable
- ConcurrentHashMap
- 1.8 死鎖
一、JUC(java.util.concurrent) 的常?類
博客結尾附有此篇博客的全部代碼!!!
1.1 Callable 接?
Callable 接口是 Java 中用于定義可以返回結果的任務的接口,它位于 java.util.concurrent 包中。
public interface Callable<V> {V call() throws Exception;
}
實例應用:計算1+2+…+100的值,使用Callable接口
public static void main(String[] args) throws InterruptedException, ExecutionException {Callable<Integer> callable = new Callable<Integer>() {public Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 100; i++) {sum += i;}return sum;}};Thread thread = new Thread(callable);thread.start();}
原因:Thread本身不提供接受結果的方法,需要FutureTask對象來拿到結果(Thread不提供接受結果是為了更好的解耦合,將任務和線程分離開)
- FutureTask:FutureTask 實現了 Runnable 接口,因此可以被 Thread 接受。
- Thread類的構造函數可以接受一個 Runnable 對象,但不能接受其他類型的對象,因為 Thread 的內部邏輯是基于 Runnable 的 run() 方法實現的。
修改:
public class CallableDemo {public static void main(String[] args) throws InterruptedException, ExecutionException {Callable<Integer> callable=new Callable<Integer>() {public Integer call() throws Exception {int sum=0;for (int i = 0; i <= 100; i++) {sum+=i;}return sum;}};FutureTask<Integer> futureTask=new FutureTask<>(callable);Thread thread=new Thread(futureTask);thread.start();System.out.println(futureTask.get());}
}
通過Runnable接口計算1+2+…+100的值:
public class RunnableDemo {private static int total=0;public static void main(String[] args) throws InterruptedException {Runnable r = new Runnable(){int sum=0;public void run() {for (int i = 0; i <=100 ; i++) {sum+=i;}total=sum;}};Thread t1 = new Thread(r);t1.start();t1.join();System.out.println(total);}
}
1.2 ReentrantLock
可重?互斥鎖. 和 synchronized 定位類似, 都是?來實現互斥效果, 保證線程安全
ReentrantLock 的核心功能是通過 Lock 接口實現的,它提供了以下方法:
- lock():獲取鎖,如果鎖已經被其他線程占用,則當前線程會阻塞,直到獲取鎖。
- unlock():釋放鎖。
- tryLock():嘗試獲取鎖,如果鎖可用則立即獲取,否則返回 false,不會阻塞。
- tryLock(long timeout, TimeUnit unit):嘗試獲取鎖,如果在指定時間內無法獲取鎖,則返回 false。
- isHeldByCurrentThread():判斷當前線程是否持有該鎖。
- isLocked():判斷鎖是否被任何線程持有。
public class ReentrantLockDemo1 {private static int total = 0;public static void main(String[] args) throws InterruptedException {ReentrantLock locker = new ReentrantLock();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {locker.lock();total++;locker.unlock();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {locker.lock();total++;locker.unlock();}});t1.start();t2.start();t1.join();t2.join();System.out.println(total);}
}
運行結果:total=100000
這里需要注意的:
因為這里解鎖需要自己手動解鎖,但是不可避免的拋出異常而導致代碼運行終止,有可能就執行不到 locker.lock();
改進:將unlocker.lock();放入finally代碼塊中
ReentrantLock和synchronized對比:
- synchronized是關鍵字,ReentrantLock是Java的標準庫中的類
- synchronized是通過代碼塊執行加鎖解鎖,而ReentrantLock是通過lock()和unlock()加鎖解鎖,需要注意的是unlock()不調用問題
- ReentrantLock提供的tryLock(),如果成功加鎖,返回true;反之,加鎖失敗,返回false,不會出現阻塞;而且還可以設置等待時長,在這段時間后再嘗試加鎖,返回true/false。
- synchronized是非公平鎖,ReentrantLock默認是非公平鎖,但是可以設置為公平鎖
ReentrantLock lock = new ReentrantLock(true);
- 更強?的喚醒機制. synchronized 是通過 Object 的 wait / notify 實現等待-喚醒. 每次喚醒的是?個隨機等待的線程. ReentrantLock 搭配Condition 類實現等待-喚醒, 可以更精確控制喚醒某個指定線程。
1.3 原子類
原子類通過提供一系列線程安全的變量操作方法,確保在多線程環境下對變量的讀寫操作是不可分割的(即原子的)。它們利用了底層硬件的原子操作指令(如 CAS),從而避免了鎖的開銷,提高了性能。
原子類的特性:
- 無鎖并發:原子類通過 CAS 機制實現線程安全,無需使用重量級的鎖(如 synchronized 或 ReentrantLock)。
- 高性能:由于避免了鎖的開銷,原子類在高并發場景下通常比傳統同步機制性能更高。
- 線程安全:原子類保證了對變量的操作是原子的,即使在多線程環境下也不會出現競態條件。
常見原子類:
(1)基本類型原子類:
AtomicInteger:用于原子操作的整數。
AtomicLong:用于原子操作的長整型。
AtomicBoolean:用于原子操作的布爾值。
(2)引用類型原子類:
AtomicReference:用于原子操作的對象引用。
AtomicStampedReference:用于原子操作的對象引用,同時帶有版本號(用于解決 ABA 問題)。
AtomicMarkableReference:用于原子操作的對象引用,同時帶有布爾標記。
(3)數組類型原子類:
AtomicIntegerArray:用于原子操作整型數組。
AtomicLongArray:用于原子操作長整型數組。
AtomicReferenceArray:用于原子操作對象引用數組。
原子類的實例:
基本類型原子類:AtomicInteger:用于原子操作的整數
public class AtomicIntegerArrayDemo1 {public static void main(String[] args) {AtomicInteger atomicInt = new AtomicInteger(2);atomicInt.incrementAndGet(); // 增加 1atomicInt.addAndGet(2); // 增加 5atomicInt.compareAndSet(5, 10); // 如果當前值為 5,則設置為 10System.out.println(atomicInt.get());//這里獲取的是10}
}
public class AtomicIntegerArrayDemo {public static void main(String[] args) throws InterruptedException {AtomicInteger atomicInt = new AtomicInteger(0);Thread t1 = new Thread(() -> {for(int i = 0; i < 5000;i++ ){atomicInt.incrementAndGet();}});Thread t2 = new Thread(() -> {for(int i = 0; i < 5000;i++ ){atomicInt.incrementAndGet();}});t1.start();t2.start();t1.join();t2.join();System.out.println(atomicInt.get());//獲取的是10000}
}
引用類型原子類:AtomicStampedReference:用于原子操作的對象引用,同時帶有版本號。
public class AtomicStampedReferenceDemo1 {public static void main(String[] args) {AtomicStampedReference<String> ref = new AtomicStampedReference<>("Hello", 0);ref.compareAndSet("Hello", "World",0, 1); // 更新引用和版本號System.out.println(ref.getReference());//expectedStamp和initialStamp相等,// 則更新initialRef引用值為newReference,并且更新版本號}
}
compareAndSet 方法的作用:
- 檢查當前引用值是否為 “Hello”。
- 檢查當前版本號是否為 0。
- 如果兩個條件都滿足,則將引用值更新為 “World”,版本號更新為 1
數組類型原子類:AtomicReferenceArray:用于原子操作對象引用數組。
public class AtomicReferenceArrayDemo {public static void main(String[] args) {AtomicReferenceArray<String> array = new AtomicReferenceArray<>(new String[]{"Hello", "World"});array.set(1, "Java");//將索引為1的引用改為JavaSystem.out.println(array.get(1));}
}
1.4 線程池
線程池
1.5 信號量 Semaphore
Semaphore 的核心思想是通過一組許可證(permits)來控制對資源的訪問。每個線程在訪問資源之前,必須先獲取一個許可證;訪問完成后,釋放許可證。許可證的數量是有限的,當許可證用完時,后續的線程將被阻塞,直到有許可證被釋放。
代碼實例
public class SemaphoreDemo {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(5);System.out.println("使用第一個許可證");semaphore.acquire();System.out.println("使用第二個許可證");semaphore.acquire();System.out.println("使用第三個許可證");semaphore.acquire();System.out.println("使用第四個許可證");semaphore.acquire();
// semaphore.release();semaphore.acquire();System.out.println("使用第五個許可證");}
}
將許可證改為4張,任務還是5個:
這里可以通過jconsole.exe來調試看下運行結果:
還是四張許可證,但是這里釋放了一張許可證:
1.6 CountDownLatch
使用多線程,經常將一個大的任務分成多個子任務,使用多線程執行子任務,提高執行效率。
怎么判斷子任務全部執行完畢呢?
這里就可以用CountDownLatch來記錄各個任務完成。
- 構造 CountDownLatch 實例, 初始化 10 表?有 10 個任務需要完成.
- 每個任務執?完畢, 都調? latch.countDown() . 在 CountDownLatch 內部的計數器同時?減.
- 主線程中使? latch.await(); 阻塞等待所有任務執?完畢. 相當于計數器為 0 了
代碼實例
public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(3);Thread t1 = new Thread(()->{for(int i=0;i<3;i++){try {Thread.sleep((long) (Math.random() * 2000));latch.countDown();} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();latch.await(); // 阻塞主線程,直到計數器為 0System.out.println("所有任務執行完畢");}
}
1.7 線程安全的集合類
Vector, Stack, HashTable, 是線程安全的(不建議?), 其他的集合類不是線程安全的
多線程環境使? ArrayList
讓ArrayList變成線程安全:
- ??使?同步機制 (synchronized 或者 ReentrantLock)
- Collections.synchronizedList(new ArrayList);
返回List的各種關鍵方法都帶synchronized,這種做法類似于Vector, Stack - 使? CopyOnWriteArrayList
讀操作:讀操作直接訪問底層數組,不需要加鎖,因此性能很高。
寫操作:
- 創建底層數組的完整副本。
- 在副本上進行修改操作。
- 將副本替換為原始數組。
這種操作的效率相對低效,因為每次都需要復制整個數組。
多線程環境使?哈希表
HashMap 本?不是線程安全的.
在多線程環境下使?哈希表可以使?:
? Hashtable
? ConcurrentHashMap
Hashtable
- 使用全局鎖(synchronized)保護整個哈希表(這意味著在任何時刻,只有一個線程可以修改哈希表,其他線程必須等待),所有操作(包括讀寫)都會鎖住整個表。
- 這種機制簡單但效率低下,尤其是在高并發場景下,容易導致線程阻塞。
存在缺點:
- 如果多線程訪問同?個 Hashtable 就會直接造成鎖沖突.
- size 屬性也是通過 synchronized 來控制同步, 也是?較慢的.
- ?旦觸發擴容, 就由該線程完成整個擴容過程. 這個過程會涉及到?量的元素拷?, 效率會?常低.
ConcurrentHashMap
- 使用分段鎖(Segment)機制,將哈希表分為多個段,每個段有自己的鎖。
- JDK 1.8 以后,進一步優化為基于 CAS 和 synchronized 的鎖機制,結合數組 + 鏈表 + 紅黑樹的數據結構。
- 讀操作通常不需要加鎖,寫操作的鎖粒度更細,大大減少了鎖競爭。
優化擴容:
- 發現需要擴容的線程, 只需要創建?個新的數組, 同時只搬?個元素過去.
- 擴容期間, 新?數組同時存在.
- 后續每個來操作 ConcurrentHashMap 的線程, 都會參與搬家的過程. 每個操作負責搬運??部分元素.
- 搬完最后?個元素再把?數組刪掉.
- 這個期間, 插?只往新數組加.
- 這個期間, 查找需要同時查新數組和?數組
1.8 死鎖
線程安全
此篇博客的全部代碼!!!