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等工具,連接對應程序后排查死鎖。
預防死鎖:
- 破壞互斥條件:一般不太可行,因為很多資源本身就是天然互斥的。
- 破壞請求與保持條件:可以要求進程一次性請求所有需要的資源,而不是逐步請求。
- 破壞不可剝奪條件:當一個進程請求新的資源得不到滿足時,釋放已占有的資源。
- 破壞循環等待條件:可以對資源進行編號,規定進程只能按照編號遞增的順序請求資源。
避免死鎖:
銀行家算法:通過提前判斷資源分配的安全性,來決定是否為進程分配資源。
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關鍵字有三種使用方法:
- 修飾實例方法:獲取對象鎖
- 修飾靜態方法:獲取類鎖
- 修飾代碼塊:取決于括號里的參數,如果是類就是類鎖,如果是某個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