Java集合+并發(部分)

Java集合

Java集合類的繼承結構和各自的適用情況

Collection

? — List

? — ArrayList:動態數組

? — LinkedList:底層是雙向鏈表,應用于Queue接口可以用于實現隊列,應用于Deque接口可以用于實現棧

? — Vector:線程安全的動態數組,但由于性能較低已被廢棄(其下有Stack)

? — Set

? — HashSet:哈希集合,底層數據結構是哈希表

? — LinkedHashSet:底層數據結構是哈希表+鏈表,可以保證先進先出

? — TreeSet:底層數據結構是紅黑樹

? — Queue

? — PriorityQueue:優先隊列

? — Deque:Double Ended Queue,雙端隊列,可以在隊列兩端進行插入、刪除操作,因此既可以作為隊列,也可以作為棧

? — ArrayDeque

? — LinkedList

Map

? — HashMap(哈希表)

List適用于需要順序存儲的情況

Set適用于需要存儲無順序、不重復的情況

Queue適用于隊列存儲

Map適用于存儲鍵值對

List

1. ArrayList底層實現

ArrayList底層是Object數組。

ArrayList實現了以下接口:

List:是一個順序列表

RandomAccess:支持快速隨機訪問

Cloneable:支持深拷貝、淺拷貝

Serializable:支持序列化

觀察ArrayList源碼,默認長度是10,默認一開始是空數組,當插入第一個元素時,數組長度變為默認的10。當繼續插入元素時,如果數組中元素達到長度極限,調用grow方法擴容數組,如果沒有達到極限,直接插入。

觀察ArrayList的構造方法,有三種。一種是什么都不傳入,默認是空數組。第二種是傳入容量,則創建對應大小的數組。第三種是傳入Collection接口下的集合,將其他集合類型轉為ArrayList。

2. ArrayList的擴容原理

當調用add方法添加元素時,首先調用ensureCapacityInternal方法,判斷當前元素數量和當前數組長度的關系。如果數組夠用,繼續插入元素。如果數組不夠用,調用grow方法進行數組擴容。

grow方法:

使用位運算將數組容量擴充為原來的1.5倍,檢查是否滿足需求,如果滿足,將新容量作為數組容量,如果不滿足,將最小需求作為數組容量。調用Arrays.copyOf方法創建一個擁有新容量的數組,并把原數組復制過去。

3. ArrayList與Vector的區別

ArrayList是線程不安全的,Vector內部調用了synchronized關鍵字,是線程安全的

4. ArrayList插入、刪除元素的時間復雜度是多少

頭部插入元素:所有元素右移,時間復雜度O(n)

尾部插入元素:如果不需要擴容,時間復雜度O(1);如果需要擴容,需要復制元素到新數組,時間復雜度O(n)

某一位置插入元素:右邊所有元素右移,時間復雜度O(n)

頭部刪除元素:所有元素左移,時間復雜度O(n)

尾部刪除元素:O(1)

某一位置刪除元素:右邊所有元素右移,時間復雜度O(n)

5. ArrayList是否實現了RandomAccess接口

RandomAccess是一個標記接口,表示該類支持快速隨機訪問(也就是通過索引在O(1)時間內快速訪問某元素)。

ArrayList可以通過索引訪問,是支持RandomAccess接口的。

6. LinkedList的底層原理是什么

LinkedList的底層原理是雙向鏈表,內部的鏈表節點中存儲:本節點值data、前一個節點指針prev、后一個節點指針next。

LinkedList實現的接口:

List:支持順序存儲

Deque:支持雙端隊列,由此可以作為棧或隊列的底層數組結構

Cloneable:支持拷貝

Serializable:支持序列化

7. LinkedList插入、刪除元素的時間復雜度

頭部插入/刪除:O(1)

尾部插入/刪除:O(1)

指定位置插入、刪除:需要遍歷到指定位置,O(n)

8. LinkedList如何作為隊列和棧

作為隊列:

Queue<Integer> queue = new LinkedList();
queue.offer(1); // 添加元素
queue.poll();  // 彈出隊首元素
queue.peek();  // 獲取隊首元素但不彈出

作為棧:

Deque<Integer> stack = new LinkedList();
stack.push(1);  // 入棧
stack.pop();  // 出棧
stack.peek();  // 獲取棧頂元素但不彈出

另外,在回溯問題中,會使用LinkedList存儲path,在添加入path時使用path.add(),在遍歷完當前path后使用removeLast()方法撤銷。最后,在將path添加到List<> ans中時,將其轉為ArrayList,ans.add(new ArrayList(path));

9. CopyOnWriteArrayList保證線程安全的原理

ArrayList是線程不安全的,Vector是線程安全的,但Vector實現線程安全的方式是讀寫都加synchronized,性能較低,目前已被廢棄。

現在實現線程安全的動態數組的是CopyOnWriteArrayList。

CopyOnWriteArrayList保證線程安全的原理是讀寫鎖,即讀不加鎖,寫加鎖,這樣保證了讀操作的性能。

在此基礎上,使用寫時復制(CopyOnWrite, COW)策略,進一步保證了寫操作也不會影響讀操作。具體來說,當需要進行寫操作時,不會直接修改原數組,而是創建一份副本,在副本數組上進行修改,修改完后再將數組復制回去。這樣保證了寫操作不會影響讀操作。

但這樣做也有一些缺點:

1)寫操作時間、空間開銷,每次寫操作都要復制一份數組,改后再復制回去

2)可能導致數據一致性問題

Set

1. HashSet底層原理

HashSet底層是用HashMap實現的,具體來說,HashSet的元素實際上存儲在HashMap的key中,而value存儲一個固定的Object對象。

HashSet的特點是無序性和不可重復性。

無序性指的是:HashSet不按元素插入順序進行存儲,而是根據其哈希值進行存儲。

不可重復性是指:HashSet內部不允許出現重復元素,因此經常被用來去重。

2. HashSet插入、刪除元素的時間復雜度

HashSet插入、刪除元素的時間復雜度都是O(1)。

3. HashSet、LinkedHashSet、TreeSet的適用場景分別是什么

HashSet適用于無序、無重復元素場景。

LinkedHashSet適用于需要先進先出的場景。

TreeSet適用于需要自定義排序的場景。

Queue

1. Queue和Deque的區別

Queue是單端隊列,只能隊尾插入元素,隊首刪除元素。

Deque是雙端隊列,兩端都能插入、刪除。

2. ArrayQueue和LinkedList區別

ArrayQueue和LinkedList都實現了Deque接口,都可以實現雙端隊列的功能。

但二者的底層數據結構不一樣。ArrayQueue是基于動態數組和雙指針實現的,LinkedList是基于雙向鏈表實現的。

3. PriorityQueue底層數據結構是什么,插入刪除的時間復雜度是多少

底層數據結構是二叉堆,即父節點的值一定小于或等于子節點的值。PriorityQueue poll出的堆頂元素一定是最小的。

插入、刪除元素的時間復雜度是O(logn)

遍歷priorityqueue需要先將其放入ArrayList中,然后使用for-each遍歷:

List<Integer> pq_list = new ArrayList(pq);
for(Integer i : pq_list) {System.out.println(i);
}

自定義排序:

PriorityQueue<Integer> pq = new PriorityQueue<Integer>(new Comparable<Integer>(){@Overridepublic int compare(Integer i1, Integer i2) {return i2 - i1;  // 大頂堆}
});

4. ArrayBlockingQueue是什么

ArrayBlockingQueue是阻塞隊列,主要用于生產者和消費者之間的通信。

即生產者線程向其中添加元素,消費者線程從其中提取元素。

可以分別實現阻塞式和非阻塞式的put和take。

阻塞式:put和take,當隊列滿時,阻塞put操作,直至隊列有空余。當隊列空時,阻塞take操作,直至隊列有元素。

非阻塞式:offer和poll,當隊列滿時,不阻塞,而是offer方法直接返回false,添加失敗;當隊列空時,不阻塞,而是poll直接返回null,獲取到的是空。

使用BlockingQueue實現生產者—消費者模式:

class Producer extends Thread {private BlockingQueue<String> bq;public Producer(BlockingQueue<String> bq) {this.bq = bq;}@Overridepublic void run() {for (int i = 1; i <= 10; i++) {String new_product = "Product " + i;try {bq.put(new_product);System.out.println("Produced: " + new_product);} catch (InterruptedException e) {e.printStackTrace();}}}
}class Consumer extends Thread {private BlockingQueue<String> bq;public Consumer(BlockingQueue<String> bq) {this.bq = bq;}@Overridepublic void run() {for (int i = 1; i <= 10; i++) {try {String product = bq.take();System.out.println("Consumed: " + product);} catch (InterruptedException e) {e.printStackTrace();}}}
}public class Main {public static void main(String[] args) {BlockingQueue<String> bq = new ArrayBlockingQueue(10);Producer producer = new Producer(bq);Consumer consumer = new Consumer(bq);producer.start();consumer.start();}
}

5. DelayQueue是什么

DelayQueue是延遲隊列,可以用于實現延時任務,例如“訂單下單15分鐘未支付自動取消”功能。

DelayQueue中的元素必須實現Delayed接口,并重寫getDelay()方法,用于計算是否到期。DelayQueue底層是用PriorityQueue實現的,默認按照到期時間升序排列,當getDealy()方法返回值小于0時,移除隊列。

Map

1. HashMap底層原理

HashMap底層原理是哈希表+鏈表,即元素首先根據key的hashcode計算哈希值,存儲在對應哈希表中。如果出現哈希碰撞,使用拉鏈法解決。當鏈表長度大于8時,會將鏈表轉為紅黑樹,以提高搜索效率。(當整個哈希表長度不超過64時,不會轉為紅黑樹,而是進行數組擴容)

HashMap添加、刪除元素的時間復雜度:

如果沒有出現哈希沖突,是O(1)。

如果出現了哈希沖突,且哈希沖突用鏈表解決,是O(n)

如果出現了哈希沖突,且哈希沖突用紅黑樹解決,是O(logn)

2. HashMap數組擴容的原理

HashMap默認長度是16,默認負載因子是0.75,也就是說,75%的數組有數據,25%的數據無數據,這是為了盡量減少哈希碰撞。當數組元素數量>數組容量*負載因子時,會對hashmap的數組進行擴容。

數組擴容的原理:

默認將數組擴容為原來的2倍。擴容方式是:根據新的長度重新計算所有元素的哈希值,將原來的元素復制到新的哈希位置。

3. HashMap數組長度為什么是2的冪次方/為什么擴容是乘以2

1)可以用位運算進行取余操作來獲得哈希位置,效率更高

2)可以保證哈希值分布比較均勻

3)擴容時只需檢查哈希值高位來判斷是否變化位置

4. HashMap插入新元素的流程

首先,判斷哈希數組是否為空,如果為空,先擴容到默認值16.

其次,計算哈希值。

如果對應哈希位置沒有元素,直接插入在這一位置。

如果對應哈希位置有元素,判斷這一元素key與要插入的key是否相同,如果相同直接修改對應value。

如果不相同,判斷這一位置上的元素的紅黑樹節點還是鏈表節點。

如果是紅黑樹節點,調用putTreeVal放入樹中。

如果是鏈表節點,遍歷到鏈表,如果有相同的key,直接修改對應value,如果沒有,遍歷到鏈表末尾后在末尾插入新元素。在鏈表末尾插入元素后,判斷是否需求將鏈表轉為紅黑樹。

插入完成后,根據數組長度和負載因子判斷是否需要擴容。

5. JDK1.7以前的HashMap在多線程環境下為什么會導致死循環

JDK1.7以前的Hashmap鏈表,插入新元素時采用的是頭插法,而多線程環境下,多個線程同時操作鏈表,可能導致頭節點指向錯誤的位置,從而導致形成環形鏈表。

為了解決這個問題,JDK1.8以后鏈表插入新元素改為了尾插法。

但多線程環境下還是建議使用ConcurrentHashMap。因此即使沒有死循環的問題,多線程環境下也可能導致數據覆蓋、數據丟失等情況。

6. 如何遍歷hashmap

三種方法:1)使用keySet;2)使用valueSet;3)使用entrySet;

HashMap<Integer, Integer> map = new HashMap();
for(Integer key : map.keySet()){}
for(Integer value: map.valueSet()){}
for(Map.Entry<Integer, Integer> entry : map.entrySet()){}

7. ConcurrentHashMap是怎么保證線程安全的

JDK1.8以前,ConcurrentHashMap是通過分段加鎖保證線程安全的。

而JDK1.8以后,是使用CAS+synchronized關鍵字實現的,只鎖定當前數組元素或鏈表和紅黑樹的頭節點,鎖粒度更細,只要不在同一個節點,就不影響其他線程,效率更高。

當插入新元素時,先根據key計算出哈希值,如果當前位置沒有元素,使用CAS嘗試插入元素,如果失敗就不斷重復嘗試自旋插入,如果成功,直接break。

如果當前位置有元素,獲取synchronized鎖,鎖定當前鏈表/紅黑樹插入元素。

8. LinkedHashMap的底層原理

LinkedHashMap底層是使用HashMap+雙向鏈表實現的,在hashmap哈希存儲節點時,還維護了一個雙向鏈表,來存儲元素的插入順序。

當使用迭代器遍歷LinkedHashMap時,按照插入順序遍歷。

9. LinkedHashMap如何按照訪問順序存儲

當希望按照訪問順序輸出時,只需將LinkedHashMap的accessOrder設置為true,這樣,當使用get方法訪問元素時,會將其移動到鏈表末尾。當按照鏈表遍歷時,即可得到訪問順序輸出。

10. LinkedHashMap如何實現LRU緩存

LinkedHashMap可以用于實現LRU緩存。

1)繼承LinkedHashMap

2)構造方法:調用super構造方法并設置accessOrder為true,當訪問某元素時,將其移到鏈表最末尾。存儲容量。

3)重寫LinkedHashMap的removeEldestEntry方法,這個方法返回一個布爾值,告知LinkedHashMap是否需要移除鏈表head元素,在這個方法中,判斷鏈表長度是否超過容量。

Java并發

線程和進程

1. 什么是進程,什么是線程,Java中是如何規定的

進程是內存分配的基本單位,線程是CPU調度的基本單位。線程是輕量級的進程,執行開銷小,但因為共享部分資源,不利于資源的管理和保護。

Java中多個線程共享進程的堆和方法區,但每個線程有獨立的程序計數器、虛擬機棧、本地方法棧。

Java中的線程是內核級線程,一個Java線程對應一個內核線程,可以利用多核CPU。

程序計數器為什么是私有的?

程序計數器用于指示線程執行的位置,保證線程切換時能夠恢復到正確的位置,因此必須是線程私有的。

虛擬機棧用于存儲線程執行中的局部變量、操作數棧、常量池引用等信息,每個線程都不一樣,所以是線程私有的。

本地方法棧與虛擬機棧類似,但虛擬機棧存儲的是Java方法執行信息,本地方法棧存儲的是Native方法執行信息。

2. 線程的生命周期

初始態(NEW):線程剛被創建,還沒有調用start方法執行

運行態(RUNNABLE):線程調用start方法執行

阻塞態(BLOCKED):線程被鎖阻塞

等待態(Waiting):線程需要等待其他線程通知或中斷,調用wait方法可以進入該狀態

超時等待態(TIME_WAITING):同樣等待其他線程動作,但超時后自動返回。使用sleep(long millis)或wait(long millis)可以進入該狀態

終止態(TERMINATED):線程運行完畢

3. Java中如何創建線程

有兩種方法:

1)繼承Thread類

public class MyThread extends Thread {@Overridepublic void run() {// }
}MyThread myThread = new MyThread();
myThread.start();

2)實現Runnable接口

public class MyRunnable implements Runnable {@Overridepublic void run() {//}
}
Thread myThread = new Thread(new MyRunnable());
myThread.start();

4. 什么是線程的上下文,什么時候會發生上下文切換

線程的上下文就是線程執行所需的程序計數器、虛擬機棧等信息。

線程上下文切換就是指當切換線程時,先保存當前線程的上下文信息,然后將下一個線程的上下文信息加載到CPU中。

線程切換的幾種情況:

1)調用sleep()、wait()方法時主動讓出CPU

2)時間片用完

3)發生阻塞,例如IO阻塞

4)被終止或線程結束運行

5. Thread的sleep方法和Object類的wait方法有什么異同

相同:二者都會暫停線程的執行,讓出CPU

不同:sleep方法不會釋放鎖,wait方法會釋放鎖。wait后線程不會主動蘇醒,必須有其他線程notify;sleep到時間后線程會自動蘇醒。

6. 可以直接調用Thread類的run方法來啟動線程嗎

不能。直接調用Thread類的run方法只是將這個方法當作main線程中的一個普通方法去調用,不會啟動新線程。而調用Thread類的start方法時,會啟動一個新線程,執行相應準備工作,然后調用run方法。

7. 什么是并發,什么是并行

并發:多個線程在同一時間段內同時執行

并行:多個線程在同一時刻同時執行

8. 什么是同步,什么是異步

同步:發出一個調用后必須等待調用返回才能繼續執行

異步:發出一個調用后不等待返回,直接向下執行

9. 為什么要使用多線程

1)從計算機底層角度來說,對于單核CPU,在執行耗費時間的IO操作時可以切換到其他線程,提高CPU利用率;對于多核CPU,可以有效利用多核CPU的能力。

2)從互聯網發展趨勢來說,多線程并發編程是提高實現系統高并發的基礎。

10. Java使用的系統線程調度方式是什么

操作系統調度線程有兩種方式:搶占式和協同式。

搶占式:操作系統決定何時切換線程,會造成上下文切換的開銷,但公平性較好,不容易出現線程阻塞;

協同式:線程自己決定何時切換并通知系統,上下文切換開銷較小,但公平性較差。

Java使用的是搶占式,JVM本身不負責線程調度,而是交給操作系統。

11. 單核CPU能運行多線程嗎,一定能夠提高效率嗎

單核CPU可以運行多線程,操作系統通過時間片輪轉的方式將CPU時間分配給不同線程。

對于IO密集型任務,可以提高效率,因為在執行IO任務時可以將CPU給其他線程使用;

對于CPU密集型任務,不能提高效率,因為大量線程都需要使用CPU,進行線程切換反而會造成上下文切換的開銷。

12.什么是死鎖,死鎖形成的四個條件,怎樣檢測、預防、避免死鎖

死鎖是指線程循環等待其他線程持有的鎖,導致多個線程被同時阻塞。

死鎖形成的四個條件:

1)互斥條件:一個鎖不能被多個線程占有

2)請求和保持條件:線程已占用一些資源,且同時請求另一些資源,而且等待資源時不釋放已有資源

3)不剝奪條件:線程占有的資源未使用完之前不會被其他線程強行剝奪,只能自己使用完畢后釋放

4)循環等待條件:多個線程構成循環等待鏈

檢測死鎖:

可以用jmap、jstack等查看堆棧內存的分配情況;或使用Visual VM、JConsole等工具,連接對應程序后排查死鎖。

預防死鎖:

  1. 破壞互斥條件:一般不太可行,因為很多資源本身就是天然互斥的。
  2. 破壞請求與保持條件:可以要求進程一次性請求所有需要的資源,而不是逐步請求。
  3. 破壞不可剝奪條件:當一個進程請求新的資源得不到滿足時,釋放已占有的資源。
  4. 破壞循環等待條件:可以對資源進行編號,規定進程只能按照編號遞增的順序請求資源。

避免死鎖:

銀行家算法:通過提前判斷資源分配的安全性,來決定是否為進程分配資源。

JMM

1. JMM是什么

JMM是Java內存模型,它是Java程序與JVM實際內存區域之間的一種抽象的模型,并不是真實存在的。

JMM將JVM內存區域分為主內存和本地內存。主內存是所有線程共有的,所有線程創建的對象實例都存儲在主內存中。本地內存是線程私有的,本地內存中存儲的是主內存中共享變量的副本。

2. happens-before原則和指令重排序

happens-before原則按照程序順序規則、解鎖規則、volatile變量規則、傳遞規則、線程啟動規則等梳理指令的順序。

指令重排序時根據梳理出的happens-before規則來進行。

a happens-before b的含義是,a指令的結果對b是可見的

3. 并發編程的三個特性

原子性:一個原子操作要么不執行,如果執行就要執行完

可見性:線程對共享變量的修改對所有線程都是可見的。Java中用volatile關鍵字保證變量的可見性,底層原理是強制變量是主存中分配

有序性:JVM編譯器進行指令重排序時,會保證按照happens-before原則進行,這是單線程環境下是正確的,但多線程環境下不一定保證正確

Java并發常用關鍵字和類

1. volatile關鍵字有什么作用

volatile關鍵字有兩個作用:

1)保證變量的可見性,指示 JVM,這個變量是共享且不穩定的,每次使用它都到主存中進行讀取;

2)禁止指令重排序,如果將一個變量聲明為volatile,在對這個變量進行讀寫時,會插入特定的內存屏障的方式來禁止指令重排序。

volatile只能保證變量的可見性,不能保證對變量操作的原子性。

2. 雙重校驗鎖實現單例模式

public class Singleton {private volatile static Singleton singleInstance;private Singleton() {}public static Singleton getSingleInstance() {if (singleInstance == null) {synchronized(Singleton.class) {if (singleInstance == null) {singleInstance = new Singleton();}}}return singleInstance;}
}

第一次校驗:在同步代碼塊之外校驗單例是否為空,如果不為空直接返回,避免進入同步代碼塊;

第二次校驗:在進入同步代碼塊之后,再次校驗單例是否為空,避免多個線程同時通過第一次校驗進行同步代碼塊,導致創建多個實例。

另外,使用volatile關鍵字的作用:1)保證單例變量的修改對所有線程可見,避免出現一個線程初始化變量后,其他線程檢測到的仍然是null的情況。2)禁止指令重排序,避免出現已分配內存還沒初始化的時候,由于指令重排序,直接返回了沒有初始化的內存地址的情況。

3. 樂觀鎖和悲觀鎖是什么,各有什么優勢和缺點,適合什么場景

樂觀鎖是指總是假設最好的情況,認為共享資源每次被訪問都不會出現問題,因此不加鎖,只是在提交修改時驗證資源是否被其他線程修改了,驗證方法:CAS算法/版本號機制。

悲觀鎖,總是假設最壞的情況,認為共享資源每次被訪問都會出現問題,因此每次操作都加鎖。

樂觀鎖可以避免鎖競爭造成線程阻塞,也不會有死鎖問題。但當寫占比非常多的情況下,沖突頻繁發生,會不斷嘗試自旋重試,因此會導致頻繁失敗重試影響性能。因此樂觀鎖更適用于讀操作較多,寫操作較少的場景。

悲觀鎖可以避免頻繁失敗重試影響性能,但無法避免鎖的開銷。因此悲觀鎖更適用于寫操作較多,讀操作較少的場景。

4.樂觀鎖的實現方式:CAS算法

CAS算法:Compare and Swap,當變量當前值等于期望值時,認為沒有線程修改過變量,可以更新。如果不相等,自旋重復嘗試。

Java中CAS算法是用Unsafe類實現的,提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong等方法,這些方法都是native方法,說明是用本地代碼實現的,通常是C或C++

CAS算法可能的問題:

1)ABA問題

即變量先是A,被其他線程改成了B,又被另一個線程改回了A。此時與期望值仍然相同,CAS算法誤以為沒有被其他線程修改過。

2)循環開銷時間大

由于 CAS 操作可能會因為并發沖突而失敗,因此通常會與while循環搭配使用,在失敗后不斷重試,直到操作成功。這就是自旋鎖機制,這會導致循環開銷。

3)只能保證一個共享變量的原子操作

CAS操作僅對單個共享變量有效。

解決ABA問題的方法:

版本號機制:在變量前加版本號或時間戳,先檢測變量值是否等于預期值,再檢查變量版本號是否等于預期版本號。版本號一般是在前面加一個version字段,每次修改version++。

4. synchronized關鍵字是什么,有什么作用,如何使用,底層原理是什么

synchronized關鍵字用于保證資源訪問同步性,它可以保證任何時刻只有一個線程進入代碼塊。

synchronized關鍵字有三種使用方法:

  1. 修飾實例方法:獲取對象鎖
  2. 修飾靜態方法:獲取類鎖
  3. 修飾代碼塊:取決于括號里的參數,如果是類就是類鎖,如果是某個object對象或this,就是對象鎖

synchronized關鍵字的底層原理:

synchronized修飾代碼塊時,本質是使用monitorenter指令和monitorexit指令,其中monitorenter指向代碼塊起始位置,monitorexit指向代碼塊結束位置。

synchronized修飾方法時,沒有使用monitorenter和monitorexit指令,而是給方法添加ACC_SYNCHRONIZED標識,表示該方法是一個同步方法。

但無論是使用monitorenter/monitorexit還是使用ACC_SYNCHRONIZED標識,本質都是獲取JVM的監視器鎖Monitor。

5. synchronized鎖升級

每個對象頭中會有有個Mark Word,用于記錄哈希碼、鎖狀態、GC分代年齡等信息。

對象的鎖狀態有四種:

1)無鎖狀態。

2)偏向鎖:當只有一個線程訪問同步代碼塊時,會使用偏向鎖;

3)輕量級鎖:當多個線程競爭鎖時,JVM 會將偏向鎖升級為輕量級鎖。輕量級鎖通過 CAS 操作嘗試獲取鎖,避免線程阻塞。

4)重量級鎖:當自旋等待超過一定次數后,JVM 會將鎖升級為重量級鎖。重量級鎖會導致線程阻塞,進入操作系統內核態,性能開銷較大。

synchronized修飾的代碼塊會使鎖隨線程數增多不斷升級,鎖只能升級不能降級。

synchronized和volatile的關系

synchronized和volatile是互補的,經常互相配合使用。

synchronized用于修飾代碼塊,保證多線程訪問資源的同步性。volatile用于修飾變量,保證變量的可見性。

volatile只能保證可見性和有序性,不能保證原子性。synchronized三者都能保證。

volatile比synchronized更輕量級。

6. ReentrantLock是什么

ReentrantLock是可重入鎖,即如果一個線程已經占有這個鎖,當它再次請求這個鎖時可以請求到。

實際上synchronized鎖也是可重入的,但ReetrantLock比synchronized功能更完善。

ReentrantLock有公平鎖和非公平鎖兩種實現,默認使用非公平鎖。

ReentrantLock底層是用AQS實現的。

7. synchronized與ReentrantLock的異同

相同:都是可重入鎖

不同:

1)ReentrantLock可以實現公平鎖也可以實現非公平鎖,synchronized只能實現非公平鎖;

2)synchronized基于JVM實現,ReentrantLock基于JDK實現

3)ReentrantLock還提供了等待中斷、超時、條件等高級功能。

7. 公平鎖、非公平鎖;可中斷鎖、不可中斷鎖;共享鎖、獨占鎖

公平鎖:按照申請順序分配鎖,保證了時間上的絕對順序,但性能較差;

非公平鎖:不按申請順序,而是按照一定的優先級分配鎖。

可中斷鎖:獲取鎖的過程中可以被中斷,不需要一直等到獲取鎖之后才能進行其他邏輯處理。ReentrantLock就屬于是可中斷鎖。

不可中斷鎖:一旦線程申請了鎖,就只能等到拿到鎖以后才能進行其他的邏輯處理。 synchronized就屬于是不可中斷鎖。

共享鎖:一把鎖可以被多個線程同時獲得,比如讀鎖是共享鎖

獨占鎖:一把鎖只能被一個線程獲得,比如寫鎖是獨占鎖

8. 讀寫鎖

讀寫鎖既可以保證多個線程同時讀的效率,同時又可以保證有寫入操作時的線程安全。

一般鎖的互斥條件:讀讀互斥、讀寫互斥、寫寫互斥

讀寫鎖的互斥原理:讀讀不互斥、讀寫互斥、寫寫互斥

Java中實現的讀寫鎖:ReentrantReadWriteLock、StampedLock。其中ReentrantReadWriteLock是可重入的讀寫鎖,StampedLock是不可重入的讀寫鎖。

讀寫鎖適用于讀多寫少的場景。

AQS

AQS全稱是AbstractQueueSynchronizer,即抽象隊列同步器。

顧名思義,AQS是一個用于構建鎖和同步器的抽象類,主要提供了可重入鎖、信號量、倒計時器等一些通用框架。

AQS的底層原理

AQS底層是基于CLH鎖隊列的變體實現的。

CLH鎖是一種基于CAS算法的優化。在CAS算法中,獲取不到鎖時會自旋重復獲取。這種自旋方式可能導致某個線程長時間獲取不到鎖,造成饑餓。

為了解決這個問題,CLH引入了一個隊列來組織并發競爭的線程。每個競爭的線程按順序加入到隊列中排隊,保證公平性。

當使用CAS方法獲取鎖失敗時,先短暫自旋嘗試獲取,如果仍然失敗,阻塞線程加入隊列等待被喚醒,防止重復自旋占用CPU。

AQS中CLH鎖隊列

隊列中由各個節點構成,每個節點都有自己的狀態。

例如三個線程獲取鎖時,T1先獲取到,初始化等待隊列,其中有一個頭節點,狀態為0;當T2請求鎖時,鎖已被占用,將T2加入等待隊列中,放在頭節點的next位置,此時頭結點的狀態改為SIGNAL(-1),表示當前節點退出時需喚醒后繼節點;同樣T3申請鎖時加入等待隊列,放在T2的后面,T2狀態改為SIGNAL(-1);當T1釋放鎖時,喚醒后繼節點T2,T2獲取鎖,成為新的head。

Semaphore

用于訪問數量有限的資源,控制訪問資源的線程數量。

使用場景:數據庫連接池、線程池等數量有限的資源

CountDownLatch

CountDownLatch的作用是讓count個線程阻塞,直到所有線程執行完畢。

使用場景:確保在某些任務完成后再執行后續操作,例如多個線程完成數據加載后再進行數據處理。

CyclicBarrier

CyclicBarrier的作用是讓count個線程阻塞在屏障處,直到所有線程到達屏障。

使用場景:多線程并行計算時,在某個點合并結果再繼續計算。

Atomic原子類

Atomic原子類是指具有原子操作特性的類,使用原子類的變量在操作時要么完整執行,要么不執行。

Atomic實現原理是CAS算法。

Atomic原子類有哪些?

1)基本類型原子類:AtomicInteger、AtomicLong、AtomicBoolean

2)數組類型原子類:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

3)引用類型原子類:AtomicReference、**AtomicStampedReference**、AtomicMarkableReference

4)對象屬性修改原子類:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater原子更新引用類型中某個字段

Atomic的應用場景:

并發計數

ThreadLocal

對于ThreadLocal創建的變量,每個線程都有一個本地副本。線程內通過threadLocal.get()獲取,通過threadLocal().set()設置其值。

ThreadLocal應用場景

數據庫連接管理:每個線程可以有一個獨立的數據庫連接,避免了多個線程共享數據庫連接的問題。

ThreadLocal底層原理

每個線程內部都有一個ThreadLocalMap,當用ThreadLocal創建變量時,ThreadLocal為key,對應的變量Object類為value,存儲在map中。

ThreadLocal什么情況下會導致內存泄漏?

正常使用繼承Thread類來創建線程是不會導致內存泄漏的。因為線程的ThreadLocalMap只被當前線程引用,當前線程結束任務退出以后,這種引用消失,線程對應的ThreadLocalMap就會被GC回收。

但實際項目開發中多使用線程池來實現多線程,這種情況下使用ThreadLocal就可能導致內存泄漏。因為線程池中的線程不會退出,是循環使用的,所以對應的ThreadLocalMap的引用不會消失,ThreadLocalMap不會被GC回收。但是ThreadLocalMap內部存儲的是Entry數組,其中有多個key-value對,其中key是ThreadLocal對象,是弱引用的,value是對應的Object對象,是強引用的。當當前線程執行完畢,投入下一次使用時,弱引用的key會被GC回收,但value是強引用的,不會被回收,這就導致value對應的key為null,長期無法被回收就會導致內存泄漏。

如何避免內存泄漏?

每次使用完ThreadLocal對象后,調用remove方法將其移除ThreadLocalMap

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/894392.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/894392.shtml
英文地址,請注明出處:http://en.pswp.cn/news/894392.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

第1章 量子暗網中的血色黎明

月球暗面的危機與陰謀 量子隧穿效應催生的幽藍電弧&#xff0c;于環形山表面肆意跳躍&#xff0c;仿若無數奮力掙扎的機械蠕蟲&#xff0c;將月球暗面的死寂打破&#xff0c;徒增幾分詭異。艾麗佇立在被遺棄的“廣寒宮”量子基站頂端&#xff0c;機械義眼之中&#xff0c;倒映著…

AI-ISP論文Learning to See in the Dark解讀

論文地址&#xff1a;Learning to See in the Dark 圖1. 利用卷積網絡進行極微光成像。黑暗的室內環境。相機處的照度小于0.1勒克斯。索尼α7S II傳感器曝光時間為1/30秒。(a) 相機在ISO 8000下拍攝的圖像。(b) 相機在ISO 409600下拍攝的圖像。該圖像存在噪點和色彩偏差。©…

Python3 【高階函數】項目實戰:5 個學習案例

Python3 【高階函數】項目實戰&#xff1a;5 個學習案例 本文包含 5 個關于“高階函數”的綜合應用項目&#xff0c;每個項目都包含完整的程序代碼、測試案例和執行結果。具體項目是&#xff1a; 成績統計分析單詞統計工具簡易計算器工廠任務調度器數據管道處理 項目 1&#…

【Git】初識Git Git基本操作詳解

文章目錄 學習目標Ⅰ. 初始 Git&#x1f4a5;注意事項 Ⅱ. Git 安裝Linux-centos安裝Git Ⅲ. Git基本操作一、創建git本地倉庫 -- git init二、配置 Git -- git config三、認識工作區、暫存區、版本庫① 工作區② 暫存區③ 版本庫④ 三者的關系 四、添加、提交更改、查看提交日…

RK3568使用QT操作LED燈

文章目錄 一、QT中操作硬件設備思路Linux 中的設備文件操作硬件設備的思路1. 打開設備文件2. 寫入數據到設備3. 從設備讀取數據4. 設備控制5. 異常處理在 Qt 中操作設備的典型步驟實際應用中的例子:控制 LED總結二、QT實戰操作LED燈設備1. `mainwindow.h` 頭文件2. `mainwindo…

分布式微服務系統架構第90集:現代化金融核心系統

#1.1 深化數字化轉型&#xff0c;核心面臨新挑戰 1、架構側&#xff1a;無法敏捷協同數字金融經營模式轉型。 2、需求側&#xff1a;業務需求傳導低效始終困擾金融機構。 3、開發側&#xff1a;創新產品上市速度低于期望。 4、運維側&#xff1a;傳統面向資源型監控體系難以支撐…

使用 Spring JDBC 進行數據庫操作:深入解析 JdbcTemplate

目錄 1. Spring JDBC 簡介 2. JdbcTemplate 介紹 3. 創建數據庫和表 4. 配置 Spring JDBC 5. 創建實體類 6. 使用 JdbcTemplate 實現增、刪、改、查操作 7. Spring JDBC 優點 8. 小結 1. Spring JDBC 簡介 Spring JDBC 是 Spring 框架中的一個模塊&#xff0c;旨在簡化…

BUUCTF [Black Watch 入群題]PWN1 題解

1.下載文件 exeinfo checksec 32位 IDA32 看到關鍵函數 read兩次 第一次read的變量s在bss段&#xff1b;第二次的buf到ebp距離為 24 但是第二次的read字節只能剛好填滿返回地址 傳不進去變量 所以想到棧遷移 將棧移動到變量s所在位置上來 同時 這題開了NX 無直接的binsh和s…

CentOS 上安裝 Go (Golang)

1. 檢查系統環境 確保系統為 CentOS 7 或 CentOS 8&#xff0c;或者其他兼容的 Linux 發行版。 cat /etc/os-release2. 安裝依賴 安裝一些必要的工具&#xff1a; sudo yum update -y sudo yum install -y wget tar3. 下載 Go 從 Go 官方下載頁面獲取適用于 Linux 的最新版…

chrome源碼剖析—進程通信

Chrome 瀏覽器采用多進程架構&#xff08;multi-process architecture&#xff09;&#xff0c;這種架構使得每個瀏覽器標簽、擴展、插件、GPU 渲染等都在獨立的進程中運行。為了確保不同進程之間的高效通信&#xff0c;Chrome 使用 進程間通信&#xff08;IPC, Inter-Process …

Cubemx文件系統掛載多設備

cubumx版本&#xff1a;6.13.0 芯片&#xff1a;STM32F407VET6 在上一篇文章中介紹了Cubemx的FATFS和SD卡的配置&#xff0c;由于SD卡使用的是SDIO通訊&#xff0c;因此具體驅動不需要自己實現&#xff0c;Cubemx中就可以直接配置然后生成SDIO的驅動&#xff0c;并將SD卡驅動和…

java練習(2)

回文數&#xff08;題目來自力扣&#xff09; 給你一個整數 x &#xff0c;如果 x 是一個回文整數&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 回文數 是指正序&#xff08;從左向右&#xff09;和倒序&#xff08;從右向左&#xff09;讀都是一樣的整…

使用 Tauri 2 + Next.js 開發跨平臺桌面應用實踐:Singbox GUI 實踐

Singbox GUI 實踐 最近用 Tauri Next.js 做了個項目 - Singbox GUI&#xff0c;是個給 sing-box 用的圖形界面工具。支持 Windows、Linux 和 macOS。作為第一次接觸這兩個框架的新手&#xff0c;感覺收獲還蠻多的&#xff0c;今天來分享下開發過程中的一些經驗~ 為啥要做這個…

ComfyUI安裝調用DeepSeek——DeepSeek多模態之圖形模型安裝問題解決(ComfyUI-Janus-Pro)

ComfyUI 的 Janus-Pro 節點&#xff0c;一個統一的多模態理解和生成框架。 試用&#xff1a; https://huggingface.co/spaces/deepseek-ai/Janus-1.3B https://huggingface.co/spaces/deepseek-ai/Janus-Pro-7B https://huggingface.co/spaces/deepseek-ai/JanusFlow-1.3B 安裝…

索引的底層數據結構、B+樹的結構、為什么InnoDB使用B+樹而不是B樹呢

索引的底層數據結構 MySQL中常用的是Hash索引和B樹索引 Hash索引&#xff1a;基于哈希表實現的&#xff0c;查找速度非常快&#xff0c;但是由于哈希表的特性&#xff0c;不支持范圍查找和排序&#xff0c;在MySQL中支持的哈希索引是自適應的&#xff0c;不能手動創建 B樹的…

RK3568中使用QT opencv(顯示基礎圖像)

文章目錄 一、查看對應的開發環境是否有opencv的庫二、QT使用opencv一、查看對應的開發環境是否有opencv的庫 在開發板中的/usr/lib目錄下查看是否有opencv的庫: 這里使用的是正點原子的ubuntu虛擬機,在他的虛擬機里面已經安裝好了opencv的庫。 二、QT使用opencv 在QT pr…

29.Word:公司本財年的年度報告【13】

目錄 NO1.2.3.4 NO5.6.7? NO8.9.10? NO1.2.3.4 另存為F12&#xff1a;考生文件夾&#xff1a;Word.docx選中綠色標記的標題文本→樣式對話框→單擊右鍵→點擊樣式對話框→單擊右鍵→修改→所有腳本→顏色/字體/名稱→邊框&#xff1a;0.5磅、黑色、單線條&#xff1a;點…

【數據分析】案例03:當當網近30日熱銷圖書的數據采集與可視化分析(scrapy+openpyxl+matplotlib)

當當網近30日熱銷圖書的數據采集與可視化分析(scrapy+openpyxl+matplotlib) 當當網近30日熱銷書籍官網寫在前面 實驗目的:實現當當網近30日熱銷圖書的數據采集與可視化分析。 電腦系統:Windows 使用軟件:Visual Studio Code Python版本:python 3.12.4 技術需求:scrapy、…

數據庫對象

數據庫對象 數據庫對象是構成數據庫結構的基本單位&#xff0c;它們定義了數據庫存儲的數據類型、數據的組織方式以及數據之間的關系。在數據庫中&#xff0c;對象可以包括表&#xff0c;視圖&#xff0c;索引&#xff0c;觸發器&#xff0c;存儲過程&#xff0c;函數等多種類…

Super AGI 2025 ,人形機器人,芯片半導體,價值+量化投資最佳實踐

Super AGI 2025&#xff1a;人形機器人、芯片半導體與價值量化投資最佳實踐 關鍵詞&#xff1a;Super AGI、人形機器人、芯片半導體、價值投資、量化投資、技術趨勢、投資策略 摘要&#xff1a;本文探討了Super AGI、人形機器人和芯片半導體領域的發展前景&#xff0c;并結合價…