1. java面試筆記六:併發編程
文章目錄
1.1. 併發編程三要素
- java併發編程三要素,並舉個栗子。
1、原子性:多個操作要麼全部執行,要麼全部執行失敗,期間不能被中斷,也不存在上下文切換,線程切換會帶來原子性問題。
int num=1;//原子操作
num++;//非原子操作,從主內存讀取num到線程工作內存,進行+1,再把num寫到主內存,除非用原子類,即:java.util.concurrent.atomic裏的原子變量類。
解決方法:可以用synchronized或者Lock(ReentrantLock)來把這個多步操作變成原子操作,但是不能用volatile,volatile不能修飾有依賴值的情況。
核心思想:把一個代碼塊看做一個不可分割的整體。
原子性操作代碼
public class zkfClass {
private int num=0;
//使用Lock,每個對象都有鎖,只有獲得這個鎖纔可以進行對應的操作
Lock lock=new ReentrantLock();
public void add1() {
lock.lock();
try {
num++;
} finally{
lock.unlock();
}
}
//使用Synchronized進行加鎖
public synchronized void add2() {
num++;
}
}
2、有序性:代碼一般是按照先後順序進行執行的,但是cpu會對jvm的字節碼進行重排序,提高執行效率,在多線程情況下可能會影響結果。
3、可見性:多線程情況下會出現髒讀,因爲工作內存和主內存的差別。
解決方法:使用synchronized或者lock或者volatile能夠保證可見性。
1.2. 進程作業調度算法
- 進程作業調度算法有哪些?
1、先來先服務算法
2、短作業優先調度算法
3、高響應比優先調度算法
響應比=優先權=(等待時間+要求服務時間)/要求服務時間=總響應時間/要求服務時間。
缺點:需要計算,消耗資源。
4、時間片輪轉調度算法
5、優先級調度算法。
缺點:低優先級的很長時間纔會處理,
- 常見的線程間的調度算法是怎樣的?java是哪種?
主要分兩種:
協同式線程調度:線程執行時間由線程本身來控制,線程把自己的工作做完後主動通知系統切換到另一個線程上,優點:實現簡單,沒啥線程同步問題。缺點:一旦一個線程阻塞,後面的線程都會阻塞。
搶佔式線程調度:每個線程由系統來分配執行時間,線程切換不由線程本身來決定,(java中,Thread.yieId()可以讓出執行時間,但是無法獲取執行時間,) 線程執行時間,系統可控,也不會因爲一個線程阻塞導致整個進程阻塞。
java採用的是搶佔式線程調度。
java可以設置線程的優先級,由1到10的整數指定,當多個線程可以運行時,jvm一般會運行最高優先級的線程。(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)優先級越高的越容易被cpu獲得,但是也不是100%獲得執行。
- wait和notify是協同式調度的嗎?
不是:wait是可以讓出執行時間,notify後無法獲取執行時間,隨機等待隊列裏面隨機獲取。也是搶佔式調度。
1.3. java多線程裏面的鎖
- java中的鎖種類
1、悲觀鎖:當線程去操作數據時,總認爲別的線程會去修改數據,所以它每次拿數據時都會上鎖,別的線程去拿的時候就會阻塞。比如:synchronized
2、樂觀鎖:當線程去操作數據時,數據有一個版本號,線程A去走數據,沒有上鎖,此時線程b有可能會過來修改數據,並改變版本號。當線程A提交數據後會與之前取走的版本號進行對比是否相同,相同則修改,否則更新失敗。使用場景:優惠券和秒殺技術。
CAS是樂觀鎖,但嚴格來說並不是鎖,通過原子性來保證數據的同步,比如數據庫的樂觀鎖,通過控制版本來實現。CAS不會保證線程同步,樂觀的認爲在數據更新期間沒有其他線程影響。
悲觀鎖適合寫操作多的場景,樂觀鎖適合讀操作多的場景,樂觀鎖的吞吐量比悲觀鎖大。
3、公平鎖:指多個線程按照申請鎖的順序來獲取鎖,簡單的來說,如果一個線程組裏,能保證每個線程都能拿到鎖,比如:ReentrantLock底層是同步隊列FIFO來實現的)
4、非公平鎖:獲取鎖的方式是隨機的,保證不了每個線程都能拿到鎖,也就是存在有線程餓死,一直拿不到鎖,比如:synchronized,ReentrantLock(這個是由構造函數控制的)
非公平鎖更能高於公平鎖,更能充分利用cpu時間
5、可重入鎖:也叫遞歸鎖,在外層使用鎖後,在內層仍然可以使用,並且不發生死鎖。
6、不可重入鎖:若當前線程執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到會阻塞。
小結:可重入鎖一定程度的避免死鎖,synchronized,ReentrantLock 重入鎖。
7、自旋鎖:一個線程在獲取鎖的時候,如果鎖已經被其他線程拿去,那麼該線程就會不斷的循環等待,直到獲取鎖,才退出循環。任何時候只能有一個執行單元獲得鎖。
小結:不會發生線程狀態的切換,一直處於用戶態,減少了線程上下文切換的消耗。缺點:循環會消耗cpu
常見自旋鎖:TicketLock,CLHLock,MSCLock.
8、共享鎖:也叫S鎖、讀鎖,能查看但無法修改和刪除的一種數據鎖,加鎖後其他線程可以併發讀取,查詢數據,但是不能修改,增加,刪除數據,該鎖可被多個線程持有。用於資源數據共享。
9、互斥鎖:也叫x鎖,排它鎖,寫鎖,獨佔鎖,該鎖每一次只能被一個線程所持有,加鎖後任何線程試圖再次加鎖的線程會被阻塞,直到當前線程解鎖。
10、死鎖:兩個或兩個以上的線程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞現象,若無外力作用,它們都無法讓程序進行下去。
下面三種是jvm爲了提高鎖的獲取與釋放效率而做的優化,針對Synchronized的鎖的升級,鎖的狀態是通過對象監視器在對象頭中的字段來表明,是不可逆的過程。
11、偏向鎖:一段同步代碼,一直被一個線程所訪問,那麼該線程會自動獲取鎖,獲取鎖的代價更低。
12、輕量級鎖:當鎖是偏向鎖的時候,被其他線程訪問,偏向鎖就會升級爲輕量鎖,其他線程會通過自旋的方式嘗試獲取鎖,但是不會阻塞,且性能會高點。
13、重量級別鎖:當鎖爲輕量級鎖時,其他線程雖然自旋,但自旋不會一直進行下去,當自旋到一定次數後還沒有獲取到鎖,就會進入阻塞,該鎖升級爲重量級鎖,重量級鎖會讓其他申請的線程阻塞, 性能也會降低。
小結:上面三個鎖是線程不斷升級造成的,且不可逆。
分段鎖(ConcurrentHashMap),行鎖(針對數據庫的),表鎖(針對數據庫)。
- 寫一個例子並解決死鎖
一個死鎖例子
線上經常會產生詭異現象,有可能是死鎖,且難排查。!!!
package bingfa;
/**
* 死鎖,並解決
*@class_name:DeadLockDemo
*@comments:
*@param:
*@return:
*@author:kaifan·Zhang
*@createtime:2020年3月14日
*/
public class DeadLockDemo {
public static String locka="locka";
public static String lockb="lockb";
public void methodA() {
synchronized (locka) {
System.out.println("我是A方法中,獲得了鎖A"+Thread.currentThread().getName());
try {
//讓出cpu執行權,不釋放鎖
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (lockb) {
System.out.println("我是A方法中,獲得了鎖B"+Thread.currentThread().getName());
}
}
}
public void methodB() {
synchronized (lockb) {
System.out.println("我是B方法中,獲得了鎖B"+Thread.currentThread().getName());
try {
//讓出cpu執行權,不釋放鎖
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (locka) {
System.out.println("我是B方法中,獲得了鎖A"+Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
System.out.println("主線程開始運行:"+Thread.currentThread().getName());
DeadLockDemo deadLockDemo=new DeadLockDemo();
new Thread(()->{
deadLockDemo.methodA();
}).start();
new Thread(()->{
deadLockDemo.methodB();
}).start();
System.out.println("主線程運行結束:"+Thread.currentThread().getName());
}
}
- 死鎖的必要條件
1、互斥條件:資源不能共享,只能有一個線程使用
2、請求與保持條件:線程已經獲取了資源,再請求其他資源時,已經被其他資源佔用,但是他還不放棄現有資源。
3、不可搶佔條件:有些資源時不可搶佔的,當某個線程獲取資源後,系統不能強行回收,只能由線程自己釋放。
4、循環等待條件。:多個線程形成環形鏈,每個都佔用對方申請的下個資源。
只要發生死鎖,上面的條件都成立,只要一個不滿足,就不會發生死鎖。
- 如何解決死鎖,優化一下代碼
第一種:調整申請鎖的範圍:加鎖的時候範圍越小越好。
第二種:調整申請鎖的順序
下面是調整申請鎖的範圍
package bingfa;
public class FixDeadLockDemo {
public static String locka="locka";
public static String lockb="lockb";
public void methodA() {
synchronized (locka) {
System.out.println("我是A方法中,獲得了鎖A"+Thread.currentThread().getName());
try {
//讓出cpu執行權,不釋放鎖
Thread.sleep(2000);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
synchronized (lockb) {
System.out.println("我是A方法中,獲得了鎖B"+Thread.currentThread().getName());
}
}
public void methodB() {
synchronized (lockb) {
System.out.println("我是B方法中,獲得了鎖B"+Thread.currentThread().getName());
try {
//讓出cpu執行權,不釋放鎖
Thread.sleep(2000);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
synchronized (locka) {
System.out.println("我是B方法中,獲得了鎖A"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
System.out.println("主線程開始運行:"+Thread.currentThread().getName());
FixDeadLockDemo fixDeadLockDemo=new FixDeadLockDemo();
for(int i=0;i<10;i++) {
new Thread(()->{
fixDeadLockDemo.methodA();
}).start();
new Thread(()->{
fixDeadLockDemo.methodB();
}).start();
}
System.out.println("主線程運行結束:"+Thread.currentThread().getName());
}
}
1.4. 多線程裏面的不可重入鎖設計
- 設計一個簡單的不可重入鎖
不可重入鎖:若當前線程執行某個方法已經獲取了鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。
package chongruLock;
/**
* 不可重入鎖
*/
public class UnreentrantLock {
private boolean isLocked=false;
public synchronized void lock() throws InterruptedException {
System.out.println("進入lock加鎖"+Thread.currentThread().getName());
//判斷是否已經被鎖,如果已經鎖了則當前請求的線程進行等待。
while(isLocked) {
System.out.println("進入wait等待"+Thread.currentThread().getName());
wait();
}
//進行加鎖
isLocked=true;
}
public synchronized void unLock() {
System.out.println("進入unLock解鎖"+Thread.currentThread().getName());
isLocked=false;
//喚醒對象鎖池裏面的一個線程
notify();
}
}
public class Main {
private UnreentrantLock unreentrantLock=new UnreentrantLock();
public void methodA() {
try {
unreentrantLock.lock();
System.out.println("methodA方法獲取鎖");
methodB();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
unreentrantLock.unLock();
}
}
public void methodB() {
try {
unreentrantLock.lock();
System.out.println("methodB方法獲取鎖");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
unreentrantLock.unLock();
}
}
public static void main(String[] args) {
new Main().methodA();
}
}
- 設計一個可重入鎖
重入鎖:是一個遞歸鎖,線程A調用線程B,線程A獲取了鎖,調用線程B時同樣可以獲取鎖。
package chongruLock;
public class Main2 {
private ReenTrantLock reenTrantLock=new ReenTrantLock();
public void methodA() {
try {
reenTrantLock.lock();
System.out.println("methodA方法獲取鎖");
methodB();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
reenTrantLock.unLock();
}
}
public void methodB() {
try {
reenTrantLock.lock();
System.out.println("methodB方法獲取鎖");
//methodB();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
reenTrantLock.unLock();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
new Main2().methodA();
}
}
package chongruLock;
public class ReenTrantLock {
private boolean isLocked=false;
//記錄是否是當前線程
private Thread lockedOwner=null;
//累計加鎖次數,加鎖一次,累加一,解鎖一次,減少一。
private int lockedCount=0;
public synchronized void lock() throws InterruptedException {
System.out.println("進入lock加鎖"+Thread.currentThread().getName());
Thread thread=Thread.currentThread();
//判斷是否是同一個線程獲取鎖,引用地址比較
while(isLocked && lockedOwner!=thread) {
System.out.println("進入wait等待"+Thread.currentThread().getName());
System.out.println("當前鎖的狀態isLocked="+isLocked);
System.out.println("當前count數量lockedCount="+lockedCount);
wait();
}
//進行加鎖
isLocked=true;
lockedOwner=thread;
lockedCount++;
}
public synchronized void unLock() {
System.out.println("進入unLock解鎖"+Thread.currentThread().getName());
Thread thread=Thread.currentThread();
//線程A加的鎖,只能由線程A解鎖,其他線程B不能解鎖
if(thread==this.lockedOwner) {
lockedCount--;
if(lockedCount==0) {
isLocked=false;
lockedOwner=null;
//喚醒對象鎖池中的線程
notify();
}
}
}
}
1.5. synchronized的理解
- 介紹一些對Synchronized的理解
synchronized是解決線程安全問題,常用在同步普通方法,靜態方法,代碼塊中。
synchronized是一個非公平的可重入鎖
每個對象有一個鎖,和一個等待隊列,鎖只能被一個線程持有,其他需要鎖的線程需要阻塞等待,鎖被釋放後,對象會從隊列中取出一個並喚醒,喚醒那個線程是不確定的,不保證公平性。
synchronized:加鎖有兩種形式
1、在方法中加鎖:生成的字節碼文件中會多一個ACC_SYNCHRONIZED標誌位,當一個線程訪問方法時,會去檢查是否存在ACC_SYNCHRONIZED標識,如果存在執行線程先獲取monitor,獲取成功後才能執行方法體,方法體執行完後再去釋放monitor,在方法執行期間,其他任何線程無法再獲取同一個monitor對象,也稱爲隱式同步。
2、代碼塊中加鎖:加了synchronize關鍵字的代碼塊,生成的字節碼文件會多出monitorenter和monitorexit兩個指令,每個monitor維護一個記錄着擁有次數的計數器,未被擁有的monitor的該計數器爲0,當一個線程執行monitorenter後,該計數器自增1,當同一個線程執行monitorexit指令的時候,計數器再減去1,當計數器爲0的時候monitor將再釋放,也叫顯示同步。
兩種本質上沒有區別,底層都是通過monitor來實現同步,只是方法的同步是一種隱式的方式來實現的,無需通過字節碼來實現。
- 如何查看字節碼?
javac xxx.java
javap -v xxx.class
- jvm中對象在內存中是分三部分組成的
- jdk1.6對鎖進行了優化,有哪些優化?
jdk6之前,在線程得到鎖的資源進入了block狀態時,涉及到操作系統用戶態到核心態的切換,消耗cpu資源
jdk6之後,增加了從偏向鎖到輕量級鎖,再到重量級鎖的過渡,但是在轉爲重量級鎖後,性能仍然會降低。只能說在一定程度上降低了cpu資源的消耗,因爲減少了用戶態到核心態的切換次數。jdk6之前只有重量級鎖。
當有一個線程時,會由無鎖轉向偏向鎖,當多個進程進來競爭鎖時,會由偏向鎖轉爲輕量級鎖,沒有獲取鎖的線程會通過自旋嘗試獲取鎖,(減少了cpu資源的調度,通過線程自旋線程自動獲取鎖不需要系統給線程安排鎖),如果自旋的次數增加,就會轉爲重量級鎖,重量級鎖就是通過系統調度爲其安排了。
1.6. Compare and Swap知多少?
- 瞭解CAS嗎?能否解釋一下什麼是CAS?
CAS:全稱Compare And Swap 即:比較再交換,是實現併發應用的一種技術。
底層是通過Unsafe類實現原子性操作,包含三個操作數,內存地址(舊的)V,預期原值A,和新值B
如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值,若在第一輪循環中,a線程獲取地址裏面的值被b線程修改了,那麼a線程需要自旋,到下次循環纔有可能會執行。
CAS就是併發環境中,修改對象值的一個解決方案。
CAS是一個樂觀鎖,性能比悲觀鎖大大提高
AtomicXXX 等原子類底層就是通過CAS實現,一定程度比Synchronized好,因爲後者是悲觀鎖。
synchronize是一個悲觀,非公平,可重入的偏向鎖。
- CAS存在什麼比較嚴重的問題?
存在ABA問題,和自旋時間長cpu利用率增加的問題。
CAS裏面有一個判斷過程,如果線程一直沒有獲取狀態,cpu資源會一直被佔用。
- ABA問題?
如果一個變量v初次讀取的是A值,並且準備賦值時也是A值,那就能說明A值沒有被修改過嗎?不能,因爲變量v可能被其他線程改回A值,結果導致CAS認爲變量v從來都沒有被修改過。從而賦值給v
解決方案:
給變量添加一個版本號即可:在比較的時候不僅要比較當前變量的值,還需要比較當前變量的版本號。
jdk5中:已經提供了AtomicStampedReference來解決問題,先檢查當前引用是否等於預期引用,其次檢查當前標誌是否等於預期標誌,如果都相等就會以原子的方式將引用和標誌都設置爲新值。