java面試筆記六:併發編程,進程調度算法,線程裏面的鎖,CAS和ABA問題

1. java面試筆記六:併發編程

1.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. 進程作業調度算法有哪些?

1、先來先服務算法

2、短作業優先調度算法

3、高響應比優先調度算法

響應比=優先權=(等待時間+要求服務時間)/要求服務時間=總響應時間/要求服務時間。

缺點:需要計算,消耗資源。

4、時間片輪轉調度算法

5、優先級調度算法。

缺點:低優先級的很長時間纔會處理,

  1. 常見的線程間的調度算法是怎樣的?java是哪種?

主要分兩種:

​ 協同式線程調度:線程執行時間由線程本身來控制,線程把自己的工作做完後主動通知系統切換到另一個線程上,優點:實現簡單,沒啥線程同步問題。缺點:一旦一個線程阻塞,後面的線程都會阻塞。

​ 搶佔式線程調度:每個線程由系統來分配執行時間,線程切換不由線程本身來決定,(java中,Thread.yieId()可以讓出執行時間,但是無法獲取執行時間,) 線程執行時間,系統可控,也不會因爲一個線程阻塞導致整個進程阻塞。

java採用的是搶佔式線程調度。

java可以設置線程的優先級,由1到10的整數指定,當多個線程可以運行時,jvm一般會運行最高優先級的線程。(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)優先級越高的越容易被cpu獲得,但是也不是100%獲得執行。

  1. wait和notify是協同式調度的嗎?

不是:wait是可以讓出執行時間,notify後無法獲取執行時間,隨機等待隊列裏面隨機獲取。也是搶佔式調度。

1.3. java多線程裏面的鎖

  1. 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),行鎖(針對數據庫的),表鎖(針對數據庫)。

  1. 寫一個例子並解決死鎖

一個死鎖例子

線上經常會產生詭異現象,有可能是死鎖,且難排查。!!!

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. 死鎖的必要條件

1、互斥條件:資源不能共享,只能有一個線程使用

2、請求與保持條件:線程已經獲取了資源,再請求其他資源時,已經被其他資源佔用,但是他還不放棄現有資源。

3、不可搶佔條件:有些資源時不可搶佔的,當某個線程獲取資源後,系統不能強行回收,只能由線程自己釋放。

4、循環等待條件。:多個線程形成環形鏈,每個都佔用對方申請的下個資源。

只要發生死鎖,上面的條件都成立,只要一個不滿足,就不會發生死鎖。

  1. 如何解決死鎖,優化一下代碼

第一種:調整申請鎖的範圍:加鎖的時候範圍越小越好。

第二種:調整申請鎖的順序

下面是調整申請鎖的範圍

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. 多線程裏面的不可重入鎖設計

  1. 設計一個簡單的不可重入鎖

不可重入鎖:若當前線程執行某個方法已經獲取了鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。

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();
    }
}
  1. 設計一個可重入鎖

重入鎖:是一個遞歸鎖,線程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的理解

  1. 介紹一些對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來實現同步,只是方法的同步是一種隱式的方式來實現的,無需通過字節碼來實現。

  1. 如何查看字節碼?

javac xxx.java

javap -v xxx.class

  1. jvm中對象在內存中是分三部分組成的

jvm中的對象結構

  1. jdk1.6對鎖進行了優化,有哪些優化?

jdk6之前,在線程得到鎖的資源進入了block狀態時,涉及到操作系統用戶態到核心態的切換,消耗cpu資源

jdk6之後,增加了從偏向鎖到輕量級鎖,再到重量級鎖的過渡,但是在轉爲重量級鎖後,性能仍然會降低。只能說在一定程度上降低了cpu資源的消耗,因爲減少了用戶態到核心態的切換次數。jdk6之前只有重量級鎖。

當有一個線程時,會由無鎖轉向偏向鎖,當多個進程進來競爭鎖時,會由偏向鎖轉爲輕量級鎖,沒有獲取鎖的線程會通過自旋嘗試獲取鎖,(減少了cpu資源的調度,通過線程自旋線程自動獲取鎖不需要系統給線程安排鎖),如果自旋的次數增加,就會轉爲重量級鎖,重量級鎖就是通過系統調度爲其安排了。

1.6. Compare and Swap知多少?

  1. 瞭解CAS嗎?能否解釋一下什麼是CAS?

CAS:全稱Compare And Swap 即:比較再交換,是實現併發應用的一種技術。

底層是通過Unsafe類實現原子性操作,包含三個操作數,內存地址(舊的)V,預期原值A,和新值B

如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值,若在第一輪循環中,a線程獲取地址裏面的值被b線程修改了,那麼a線程需要自旋,到下次循環纔有可能會執行。

CAS就是併發環境中,修改對象值的一個解決方案。

CAS是一個樂觀鎖,性能比悲觀鎖大大提高

AtomicXXX 等原子類底層就是通過CAS實現,一定程度比Synchronized好,因爲後者是悲觀鎖。

synchronize是一個悲觀,非公平,可重入的偏向鎖。

CAS過程

CAS多線程過程

  1. CAS存在什麼比較嚴重的問題?

存在ABA問題,和自旋時間長cpu利用率增加的問題。

CAS裏面有一個判斷過程,如果線程一直沒有獲取狀態,cpu資源會一直被佔用。

  1. ABA問題?

如果一個變量v初次讀取的是A值,並且準備賦值時也是A值,那就能說明A值沒有被修改過嗎?不能,因爲變量v可能被其他線程改回A值,結果導致CAS認爲變量v從來都沒有被修改過。從而賦值給v

ABA問題

解決方案:

給變量添加一個版本號即可:在比較的時候不僅要比較當前變量的值,還需要比較當前變量的版本號。

jdk5中:已經提供了AtomicStampedReference來解決問題,先檢查當前引用是否等於預期引用,其次檢查當前標誌是否等於預期標誌,如果都相等就會以原子的方式將引用和標誌都設置爲新值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章