主頁 > 後端開發 > 小滴課堂并發與多執行緒相關面試題總結

小滴課堂并發與多執行緒相關面試題總結

2021-02-22 12:57:18 後端開發

1. 什么是行程、執行緒、協程,他們之間的關系是怎樣的?

  • 行程:
    • 本質上是一個獨立執行的程式,行程是作業系統進行資源分配和調度的基本概念,作業系統進行資源分配和調度的一個獨立單位,
  • 執行緒:
    • 作業系統能夠進行運算調度的最小單位,它被包含在行程之中,是行程中的實際運作單位,一個行程中可以并發多個執行緒,每條執行緒執行不同的任務,切換受系統控制,
  • 協程:
    • 又稱為微執行緒,是一種用戶態的輕量級執行緒,協程不像執行緒和行程需要進行系統內核上的背景關系切換,協程的背景關系切換是由用戶自己決定的,有自己的背景關系,所以說是輕量級的執行緒,也稱之為用戶級別的執行緒就叫協程,一個執行緒可以多個協程,執行緒行程都是同步機制,而協程則是異步 ,Java的原生語法中并沒有實作協程,目前python、Lua和GO等語言支持
  • 關系:
    • 一個行程可以有多個執行緒,它允許計算機同時運行兩個或多個程式,執行緒是行程的最小執行單位,CPU的調度切換的是行程和執行緒,行程和執行緒多了之后調度會消耗大量的CPU,CPU上真正運行的是執行緒,執行緒可以對應多個協程:

在這里插入圖片描述

2. 說下并發和并行的區別,并舉例說明

  • 并發 concurrency:
    • 一核CPU,模擬出來多條執行緒,快速交替執行
  • 并行 parallellism:
    • 多核CPU ,多個執行緒可以同時執行
    • eg: 執行緒池!
  • 并發指在一段時間內宏觀上去處理多個任務,并行指同一個時刻,多個任務確實真的同時運行,

舉例:

#### 并發:是一心多用,聽課和看電影,但是CPU大腦只有一個,所以輪著來

#### 并行:火影忍者中的影分身,有多個你出現,可以分別做不同的事情

3. java實作多執行緒有哪幾種方式,有什么不同,比較常用哪種?

3.1 繼承Thread

  • 繼承Thread,重寫里面run()方法,創建實體,執行start
  • 優點:代碼撰寫最簡單直接操作
  • 缺點:沒回傳值,繼承一個類后,沒法繼承其他的類,拓展性差
public class ThreadDemo1 extends Thread {
    @Override
    public void run() {
        System.out.println("繼承Thread實作多執行緒,名稱:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
      ThreadDemo1 threadDemo1 = new ThreadDemo1();
      threadDemo1.setName("demo1");
      // 執行start
      threadDemo1.start();
      System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
}

3.2 實作Runnable介面

  • 自定義類實作Runnable,實作里面run()方法,創建Thread類,使用Runnable介面的實作物件作為引數傳遞給Thread物件,呼叫Strat方法,
  • 優點:執行緒類可以實作多個幾介面,可以再繼承一個類
  • 缺點:沒回傳值,不能直接啟動,需要通過構造一個Thread實體傳遞進去啟動
public class ThreadDemo2 implements Runnable {
    @Override
    public void run() {
        System.out.println("通過Runnable實作多執行緒,名稱:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        Thread thread = new Thread(threadDemo2);
        thread.setName("demo2");
    	// start執行緒執行
        thread.start();
        System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
}

// JDK8之后采用lambda運算式
public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        System.out.println("通過Runnable實作多執行緒,名稱:"+Thread.currentThread().getName());
    });
    thread.setName("demo2");
    // start執行緒執行
    thread.start();
    System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
}

3.3 實作Callable介面

  • 創建callable介面的實作類,并實作call()方法,結合FutureTask類包裝Callable物件,實作多執行緒,
  • 優點:有回傳值,拓展性也高
  • 缺點:jdk5以后才支持,需要重寫call()方法,結合多個類比如FutureTask和Thread類
public class MyTask implements Callable<Object> {
    @Override
    public Object call() throws Exception {
        System.out.println("通過Callable實作多執行緒,名稱:"+Thread.currentThread().getName());
        return "這是回傳值";
    }
}

public static void main(String[] args) {
    	// JDK1.8 lambda運算式
        FutureTask<Object> futureTask = new FutureTask<>(() -> {
          System.out.println("通過Callable實作多執行緒,名稱:" +
                        			Thread.currentThread().getName());
            return "這是回傳值";
        });

     	// MyTask myTask = new MyTask();
		// FutureTask<Object> futureTask = new FutureTask<>(myTask);
        // FutureTask繼承了Runnable,可以放在Thread中啟動執行
        Thread thread = new Thread(futureTask);
        thread.setName("demo3");
    	// start執行緒執行
        thread.start();
        System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
        try {
            // 獲取回傳值
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            // 阻塞等待中被中斷,則拋出
            e.printStackTrace();
        } catch (ExecutionException e) {
            // 執行程序發送例外被拋出
            e.printStackTrace();
        }
}

3.4 通過執行緒池創建執行緒

  • 自定義Runnable介面,實作run方法,創建執行緒池,呼叫執行方法并傳入物件
  • 優點:安全高性能,復用執行緒
  • 缺點: jdk5后才支持,需要結合Runnable進行使用
public class ThreadDemo4 implements Runnable {
    @Override
    public void run() {
        System.out.println("通過執行緒池+runnable實作多執行緒,名稱:" +
                           Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
    	// 創建執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for(int i=0;i<10;i++){
            // 執行緒池執行執行緒任務
            executorService.execute(new ThreadDemo4());
        }
        System.out.println("主執行緒名稱:"+Thread.currentThread().getName());
        // 關閉執行緒池
        executorService.shutdown();
}
  • 一般常用的Runnable 和 第四種執行緒池+Runnable,簡單方便擴展,和高性能 (池化的思想)

3.5 Runable Callable Thread 三者區別?

  • Thread是一個抽象類,只能被繼承,而Runable Callable是介面,需要實作介面中的方法
  • 繼承Thread重寫run()方法,實作Runable介面需要實作run()方法,而Callable是需要實作call()方法
  • Thread和Runable 沒有回傳值,Callable 有回傳值
  • 實作Runable 介面的類不能直接呼叫start()方法,需要new 一個Thread并發該實作類放入Thread,再通過新建的Thread實體來呼叫start()方法,
  • 實作Callable 介面的類需要借助FutureTask(將該實作類放入其中),再將FutureTask實體放入Thread,再通過新建的Thread實體來呼叫start()方法,獲取回傳值只需要借助FutureTask實體呼叫get()方法即可!

4. 執行緒的幾個狀態(生命周期)?

執行緒有幾個狀態(6個)!

public enum State {
    /**
     * 執行緒新生狀態
     */
    NEW,
    
    /**
     * 執行緒運行中
     */
    RUNNABLE,
    
    /**
     * 執行緒阻塞狀態
     */
    BLOCKED,
    
    /**
     * 執行緒等待狀態,死等
     */
    WAITING,
    
    /**
     * 執行緒超時等待狀態,超過一定時間就不再等
     */
    TIMED_WAITING,
    
    /**
     * 執行緒終止狀態,代表執行緒執行完畢
     */
    TERMINATED;
}

5. 執行緒狀態轉換的相關方法:sleep/yield/join wait/notify/notifyAll

Tread下的方法

##### sleep()
    屬于執行緒Thread的方法,讓執行緒暫緩執行,等待預計時間之后再恢復
    交出CPU使用權,《不會釋放鎖》,抱著鎖睡覺!
    進入超時等待狀態TIME_WAITGING,睡眠結束變為就緒Runnable
    
##### yield()
    屬于執行緒Thread的方法,暫停當前執行緒的物件,去執行其他執行緒
    交出CPU使用權,《不會釋放鎖》,和sleep類似
    作用:讓相同優先級的執行緒輪流執行,但是不保證一定輪流
    注意:不會讓執行緒進入阻塞狀態BLOCKED,直接變為就緒Runnable,只需要重新獲得CPU使用權
    
##### join()
    屬于執行緒Thread的方法,在主執行緒上運行呼叫該方法,會讓主執行緒休眠,
    《不會釋放鎖》 讓呼叫join方法的執行緒先執行完畢,再執行其他執行緒
    類似讓救護車警車優先通過!!

Object下的方法

##### wait()
    屬于Object的方法,當前執行緒呼叫物件的wait方法,
    《會釋放鎖》,進入執行緒的等待佇列
    需要依靠notify或者notifyAll喚醒,或者wait(timeout)時間自動喚醒
    
##### notify()
    屬于Object的方法
    喚醒在物件監視器上等待的單個執行緒,《隨機喚醒》
    
##### notifyAll()
    屬于Object的方法
    喚醒在物件監視器上等待的全部執行緒,《全部喚醒》

執行緒狀態轉換流程圖

6. Java中可以有哪些方法來保證執行緒安全?

  • 加鎖:比如synchronize/ReentrantLock
  • 使用volatile宣告變數,輕量級同步,不能保證原子性(需要解釋)
  • 使用執行緒安全類,例如原子類 AtomicXXX
  • 使用執行緒安全集合容器,例如:CopyOnWriteArrayList/ConcurrentHashMap
  • ThreadLocal本地私有變數/信號量Semaphore等

7. 是否了解volatile關鍵字?能否解釋下它和synchronized有什么區別?

執行緒安全行:

執行緒安全性包括兩個方面,①可見性,②原子性

volatile特性

  • 參考文章: volatile關鍵字

  • volatile保證執行緒可見性案例:使用Volatile關鍵字的案例分析

  • 原始碼分析文章參考:java同步系列之volatile決議

通俗來說就是,執行緒A對一個volatile變數的修改,對于其它執行緒來說是可見的,即執行緒每次獲取volatile變數的值都是最新的,

二者對比

  • volatile是輕量級的synchronized,保證了共享變數的可見性,被volatile關鍵字修飾的變數,如果值發生了變化,其他執行緒立刻可見,避免出現臟讀現象!
  • volatile輕量級,只能修飾變數,synchronized重量級,還可修飾方法
  • volatile只能保證資料的可見性,不能用來同步,因為多個執行緒并發訪問volatile修飾的變數不會阻塞
    synchronized不僅保證可見性,而且還保證原子性,因為,只有獲得了鎖的執行緒才能進入臨界區,從而保證臨界區中的所有陳述句都全部執行,多個執行緒爭搶synchronized鎖物件時,會出現阻塞
  • volatile:保證可見性,但是不能保證原子性
  • synchronized:保證可見性,也保證原子性

使用場景

對變數的寫操作不依賴當前值,如多執行緒下執行a++,是無法通過volatile保證結果原子性的;

volatile int i = 0;并且大量執行緒呼叫i的自增操作,那么volatile可以保證變數的安全嗎?

不可以保證!,volatile不能保證變數操作的原子性!

  • 自增操作包括三個步驟,分別是:讀取,加一,寫入,由于這三個子操作的原子性不能被保證,那么n個執行緒總共呼叫ni++的操作后,最后的i的值并不是大家想的n,而是一個比n小的數!

  • 解釋

    • 比如A執行緒執行自增操作,剛讀取到i的初始值0,然后就被阻塞了!
    • B執行緒現在開始執行,還是讀取到i的初始值0,執行自增操作,此時i的值為1
    • 然后A執行緒阻塞結束,對剛才拿到的0執行加1與寫入操作,執行成功后,i的值被寫成1了!
    • 我們預期輸出2,可是輸出的是1,輸出比預期小!
  • 代碼實體:

    public class VolatileTest {
        public volatile int i = 0;
     
        public void increase() {
            i++;
        }
     
        public static void main(String args[]) throws InterruptedException {
            List<Thread> threadList = new ArrayList<>();
            VolatileTest test = new VolatileTest();
            for (int j = 0; j < 10000; j++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        test.increase();
                    }
                });
                thread.start();
                threadList.add(thread);
            }
     
            // 等待所有執行緒執行完畢
            for (Thread thread : threadList) {
                thread.join();
            }
            System.out.print(test.i);// 輸出9995
        }
    }
    

    總結

    volatile不需要加鎖,因此不會造成執行緒的阻塞,而且比synchronized更輕量級,而synchronized可能導致執行緒的阻塞volatile由于禁止了指令重排,所以JVM相關的優化沒了,效率會偏弱!

##### JAVA記憶體模型簡稱 JMM
	   JMM規定所有的變數存在在主記憶體,每個執行緒有自己的作業記憶體,執行緒對變數的操作都在作業記憶體中進行,不能直接對主記憶體就行操作,
	   使用volatile修飾變數,每次讀取前必須從主記憶體屬性最新的值,每次寫入需要立刻寫到主記憶體中,volatile關鍵字修修飾的變數隨時看到的自己的最新值,假如執行緒1對變數v進行修改,那么執行緒2是可以馬上看見!

8. volatile可以避免指令重排,能否解釋下什么是指令重排?

  • 指令重排序分兩類:
    • 編譯器重排序
    • 運行時重排序

JVM在編譯java代碼或者CPU執行JVM位元組碼時,對現有的指令進行重新排序,主要目的是為了優化運行效率(不改變程式結果的前提)

int a = 3;     // step:1
int b = 4;     // step:2
int c =5;      // step:3 
int h = a*b*c; // step:4

定義順序: 1,2,3,4
計算順序: 1,3,2,42,1,3,4 結果都是一樣的
  • 雖然指令重排序可以提高執行效率,但是多執行緒上可能會影響結果,有什么解決辦法?
  • 解決辦法:記憶體屏障(了解即可~)
    • 記憶體屏障是屏障指令,使CPU對屏障指令之前和之后的記憶體操作執行結果的一種約束!

擴展:現行發生原則happens-before(了解即可~)

volatile的記憶體可見性就體現了先行發生原則!

9. 介紹一下并發編程三要素?

  • 原子性
  • 有序性
  • 可見性

9.1 原子性

  • 原子性:
    • 一個不可再被分割的最小顆粒,原子性指的是一個或多個操作要么全部執行成功要么全部執行失敗,期間不能被中斷,也不存在背景關系切換,執行緒切換會帶來原子性的問題!
int num = 1; // 原子操作
num++;       // 非原子操作,從主記憶體讀取num到執行緒作業記憶體,進行+1,再把num寫回到主記憶體, 
			 // 除非用原子類:即,java.util.concurrent.atomic里的原子變數類

// 解決辦法是可以用synchronized 或 Lock(比如ReentrantLock) 來把這個多步操作“變成”原子操作
// 這里不能使用volatile,前面有說到:對變數的寫操作不依賴當前值,如多執行緒下執行a++,是無法通過volatile保證結果原子性的
public class XdTest {
    
    // 方式1:使用原子類
    // AtomicInteger  num = 0;// 這種方式的話++操作就可以保證原子性了,而不需要再加鎖了
    private int num = 0;
    
    // 方式2:使用lock,每個物件都是有鎖,只有獲得這個鎖才可以進行對應的操作
    Lock lock = new ReentrantLock();
    public  void add1(){
        lock.lock();
        try {
            num++;
        }finally {
            lock.unlock();
        }
    }
    
    // 方式3:使用synchronized,和上述是一個操作,這個是保證方法被鎖住而已,上述的是代碼塊被鎖住
    public synchronized void add2(){
        num++;
    }
}

解決核心思想:把一個方法或者代碼塊看做一個整體,保證是一個不可分割的整體

9.2 有序性

  • 有序性:
    • 程式執行的順序按照代碼的先后順序執行,因為處理器可能會對指令進行重排序JVM在編譯java代碼或者CPU執行JVM位元組碼時,對現有的指令進行重新排序,主要目的是優化運行效率(不改變程式結果的前提)
int a = 3;     // step:1
int b = 4;     // step:2
int c =5;      // step:3 
int h = a*b*c; // step:4

定義順序: 1,2,3,4
計算順序: 1,3,2,42,1,3,4 結果都是一樣的(單執行緒情況下)
指令重排序可以提高執行效率,但是多執行緒上可能會影響結果!

假如下面的場景:

// 執行緒1
before();// 處理初始化作業,處理完成后才可以正式運行下面的run方法
flag = true; // 標記資源處理好了,如果資源沒處理好,此時程式就可能出現問題
// 執行緒2
while(flag){
    run(); // 執行核心業務代碼
}

// -----------------指令重排序后,導致順序換了,程式出現問題,且難排查-----------------

// 執行緒1
flag = true; // 標記資源處理好了,如果資源沒處理好,此時程式就可能出現問題
// 執行緒2
while(flag){
    run(); // 執行核心業務代碼
}
before();// 處理初始化作業,處理完成后才可以正式運行下面的run方法

9.3 可見性

  • 可見性:
    • 一個執行緒A對共享變數的修改,另一個執行緒B能夠立刻看到!
// 執行緒 A 執行
int num = 0;
// 執行緒 A 執行
num++;
// 執行緒 B 執行
System.out.print("num的值:" + num);

執行緒A執行 i++ 后再執行執行緒B,執行緒B可能有2個結果,可能是01

因為i++ 在執行緒A中執行運算,并沒有立刻更新到主記憶體當中,而執行緒B就去主記憶體當中讀取并列印,此時列印的就是0;也可能執行緒A執行完成更新到主記憶體了,執行緒B的值是1

所以需要保證執行緒的可見性:
synchronized、lock和volatile 能夠保證執行緒可見性

volatile保證執行緒可見性案例:使用Volatile關鍵字的案例分析

10. Java里面有哪些鎖?分別解釋下

樂觀鎖/悲觀鎖

  • 悲觀鎖:
    • 當執行緒去操作資料的時候,總認為別的執行緒會去修改資料,所以它每次拿資料的時候總會上鎖,別的執行緒去拿資料的時候就會阻塞,比如synchronized
  • 樂觀鎖:
    • 每次去拿資料的時候都認為別人不會修改,更新的時候會判斷是別人是否回去更新資料,通過版本來判斷,如果資料被修改了就拒絕更新,比如CAS是樂觀鎖,但嚴格來說并不是鎖,通過原子性來保證資料的同步,比如說資料庫的樂觀鎖,通過版本控制來實作,CAS不會保證執行緒同步,樂觀的認為在資料更新期間沒有其他執行緒影響
  • 小結:悲觀鎖適合寫操作多的場景,樂觀鎖適合讀操作多的場景,樂觀鎖的吞吐量會比悲觀鎖大!

公平鎖/非公平鎖

  • 公平鎖:
    • 指多個執行緒按照申請鎖的順序來獲取鎖,簡單來說 如果一個執行緒組里,能保證每個執行緒都能拿到鎖 比如ReentrantLock(底層是同步佇列FIFO: First Input First Output來實作)
  • 非公平鎖:
    • 獲取鎖的方式是隨機獲取的,保證不了每個執行緒都能拿到鎖,也就是存在有執行緒餓死,一直拿不到鎖,比如synchronized、ReentrantLock
  • 小結:非公平鎖性能高于公平鎖,更能重復利用CPU的時間,ReentrantLock中可以通過構造方法指定是否為公平鎖,默認為非公平鎖!synchronized無法指定為公平鎖,一直都是非公平鎖,

可重入鎖/不可重入鎖

  • 可重入鎖:
    • 也叫遞回鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖,一個執行緒獲取鎖之后再嘗試獲取鎖時會自動獲取鎖,可重入鎖的優點是避免死鎖,
  • 不可重入鎖:
    • 若當前執行緒執行某個方法已經獲取了該鎖,那么在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞
  • 小結:可重入鎖能一定程度的避免死鎖 synchronized、ReentrantLock都是可重入鎖

獨占鎖/共享鎖

  • 獨享鎖,是指鎖一次只能被一個執行緒持有,

    • 也叫X鎖/排它鎖/寫鎖/獨享鎖:該鎖每一次只能被一個執行緒所持有,加鎖后任何執行緒試圖再次加鎖的執行緒會被阻塞,直到當前執行緒解鎖,例子:如果 執行緒A 對 data1 加上排他鎖后,則其他執行緒不能再對 data1 加任何型別的鎖,獲得獨享鎖的執行緒即能讀資料又能修改資料!
  • 共享鎖,是指鎖一次可以被多個執行緒持有,

    • 也叫S鎖/讀鎖,能查看資料,但無法修改和洗掉資料的一種鎖,加鎖后其它用戶可以并發讀取、查詢資料,但不能修改,增加,洗掉資料,該鎖可被多個執行緒所持有,用于資源資料共享!

ReentrantLock和synchronized都是獨享鎖,ReadWriteLock的讀鎖是共享鎖,寫鎖是獨享鎖

互斥鎖/讀寫鎖

與獨享鎖/共享鎖的概念差不多,是獨享鎖/共享鎖的具體實作,

ReentrantLock和synchronized都是互斥鎖,ReadWriteLock是讀寫鎖

自旋鎖

  • 自旋鎖:
    • 一個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那么該執行緒將回圈等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出回圈,任何時刻最多只能有一個執行單元獲得鎖,
    • 不會發生執行緒狀態的切換,一直處于用戶態,減少了執行緒背景關系切換的消耗,缺點是回圈會消耗CPU,
  • 常見的自旋鎖:TicketLock,CLHLock,MSCLock

死鎖

  • 死鎖:
    • 兩個或兩個以上的執行緒在執行程序中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法讓程式進行下去!

在這里插入圖片描述

下面三種是Jvm為了提高鎖的獲取與釋放效率而做的優化 針對Synchronized的鎖升級,鎖的狀態是通過物件監視器在物件頭中的欄位來表明,是不可逆的程序

  • 偏向鎖:
    • 一段同步代碼一直被一個執行緒所訪問,那么該執行緒會自動獲取鎖,獲取鎖的代價更低!
  • 輕量級鎖:
    • 當鎖是偏向鎖的時候,被其他執行緒訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,但不會阻塞,且性能會高點!
  • 重量級鎖:
    • 當鎖為輕量級鎖的時候,其他執行緒雖然是自旋,但自旋不會一直回圈下去,當自旋一定次數的時候且還沒有獲取到鎖,就會進入阻塞,該鎖升級為重量級鎖,重量級鎖會讓其他申請的執行緒進入阻塞,性能也會降低!

11. 寫個多執行緒死鎖的例子

執行緒在獲得了鎖A并且沒有釋放的情況下去申請鎖B,這時另一個執行緒已經獲得了鎖B,在釋放鎖B之前又要先獲得鎖A,因此倍訓發生,陷入死鎖回圈:

public class DeadLockDemo {
    private static String locka = "locka";
    private static String lockb = "lockb";
    
    public void methodA(){
        synchronized (locka){
            System.out.println("我是A方法中獲得了鎖A "+Thread.currentThread().getName() );
            // 讓出CPU執行權,不釋放鎖
            try {
                Thread.sleep(2000);// sleep不釋放鎖
            } catch (InterruptedException e) {
                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() );
            // 讓出CPU執行權,不釋放鎖
            try {
                Thread.sleep(2000);// sleep不釋放鎖
            } catch (InterruptedException e) {
                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());
    }
}

死鎖的4個必要條件:

  • 互斥條件:資源不能共享,只能由一個執行緒使用!
  • 請求與保持條件:執行緒已經獲得一些資源,但因請求其他資源發生阻塞,對已經獲得的資源保持不釋放!
  • 不可搶占:有些資源是不可強占的,當某個執行緒獲得這個資源后,系統不能強行回收,只能由執行緒使用完自己釋放!
  • 回圈等待條件:多個執行緒形成環形鏈,每個都占用對方申請的下個資源!

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

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

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

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();
    // 加鎖建議在try里面,解鎖建議在finally
    public void  methodA(){
        try {
            unreentrantLock.lock();
            System.out.println("methodA方法被呼叫");
            // methodA()中嵌套呼叫methodB(),測驗methodB()是否能獲取鎖的執行權
            methodB();
        }catch (InterruptedException e){
            e.fillInStackTrace();
        } finally {
            unreentrantLock.unlock();
        }
    }

    public void methodB(){
        try {
            unreentrantLock.lock();
            System.out.println("methodB方法被呼叫");
        }catch (InterruptedException e){
            e.fillInStackTrace();
        } finally {
            unreentrantLock.unlock();
        }
    }
    
    public static void main(String [] args){
        // 演示同一個執行緒下是否可沖入!(如果單執行緒都是不可重入的話,多執行緒下就不用說了~)
        new Main().methodA();
    }
}

// 同一個執行緒,重復獲取鎖失敗,形成死鎖,這個就是不可重入鎖

在這里插入圖片描述

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

可重入鎖:也叫遞回鎖,在外層使用鎖之后,在內層仍然可以使用,并且不發生死鎖

public class ReentrantLock {
    private boolean isLocked = false;

    // 用于記錄是不是重入的執行緒
    private Thread lockedOwner = null;

    // 累計加鎖次數,加鎖一次累加1,解鎖一次減少1
    private int lockedCount = 0;

    // 加鎖方法
    public synchronized void lock() throws InterruptedException {
        System.out.println("進入lock加鎖 "+Thread.currentThread().getName());

        // 獲取當前執行緒
        Thread thread = Thread.currentThread();

        // 判斷是否是同個執行緒獲取鎖, lockedOwner != thread參考地址的比較
        // 如果已經加鎖,且當前執行緒不是之前加鎖的執行緒則阻塞等待!
        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();
            }
        }
    }
}

public class Main {
    //private UnreentrantLock unreentrantLock = new UnreentrantLock();
    private ReentrantLock reentrantLock = new ReentrantLock();

    // 加鎖建議在try里面,解鎖建議在finally
    public void  methodA(){
        try {
            reentrantLock.lock();
            System.out.println("methodA方法被呼叫");
            methodB();
        }catch (InterruptedException e){
            e.fillInStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    public void methodB(){
        try {
            reentrantLock.lock();
            System.out.println("methodB方法被呼叫");
        }catch (InterruptedException e){
            e.fillInStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    public static void main(String [] args){
        for(int i=0 ;i<10;i++){
            // 演示的是同個執行緒
            new Main().methodA();
        }
    }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ttX91EiY-1613882316860)(小滴課堂并發與多執行緒相關面試題總結.assets/image-20210220161315283.png)]

14. 介紹下你對synchronized的理解?

原始碼分析文章參考:java同步系列之synchronized決議

  • synchronized是解決執行緒安全的問題,常用在同步普通方法、靜態方法、代碼塊中使用!
  • synchronized非公平、可重入鎖!
  • 每個物件有一個鎖和一個等待佇列,鎖只能被一個執行緒持有,其他需要鎖的執行緒需要阻塞等待,鎖被釋放后,物件會從佇列中取出一個并喚醒,喚醒哪個執行緒是不確定的,不保證公平性

15. 解釋下什么是CAS?以及ABA問題?

CAS全稱:Compare and Swap 比較并交換

Unsafe實作原理,參考文章:java魔法類之Unsafe決議

  • CAS底層通過Unsafe類實作原子性操作,操作包含三個運算元:
    • 物件記憶體地址(V):
    • 預期原值(A):
    • 新值(B)
  • 理解方式1:比較當前作業記憶體中的值和主記憶體中的值,如果這個值是期望的,那么則執行交換操作!如果不是就一直回圈!
  • 理解方式2:如果記憶體地址中的值與預期原值相匹配,那么處理器會自動將該地址的值更新為新值 ,若果在第一輪回圈中,a執行緒獲取地址里面的值被b執行緒修改了,那么a執行緒需要自旋,到下次回圈才有可能機會執行,

CAS屬于樂觀鎖,性能較悲觀鎖有很大的提高!

AtomicXXX 等原子類底層就是CAS實作,一定程度比synchonized好,因為后者是悲觀鎖

小滴老師講這塊的時候,對于第一次接觸CAS的萌新有些不好理解,這里我參考狂神老師在介紹CAS的時候的一些理解:以一個案例入手:

案例:

public class CASDemo { 
    // CAS compareAndSet : 比較并交換! 
    public static void main(String[] args) { 
        AtomicInteger atomicInteger = new AtomicInteger(2020); 
        
        // 期望、更新 
        // public final boolean compareAndSet(int expect, int update) 
        // 如果我期望的值達到了,那么就更新,否則,
        // 就不更新, CAS 是CPU的并發原語! 
        System.out.println(atomicInteger.compareAndSet(2020, 2021));// true
        System.out.println(atomicInteger.get());// 2021
        
        //atomicInteger.getAndIncrement()// 看底層如何實作 ++ 
        System.out.println(atomicInteger.compareAndSet(2020, 2021));// false
        System.out.println(atomicInteger.get());// 2021
    } 
}

我們來看一下getAndIncrement()方法的底層實作:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // UnSafe類,底層是呼叫C++:Java無法操作記憶體,所以這里借助C++來操作記憶體
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            // 獲取記憶體偏移值valueOffset
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    // value被volatile修飾,避免指令重排,且保證執行緒可見性和有序性
    private volatile int value;
    
    ...

    public final int getAndIncrement() {
        // 引數:
        // this: 當前物件
        // valueOffset:當前物件的記憶體偏移地址
        // 1:值
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    ...
}

大致了解UnSafe后,我們繼續點進getAndIncrement()方法中,unsafe呼叫的getAndAddInt()方法查看:

// 位于UnSafe類中
// 引數:var1 當前物件,var2 當前物件的記憶體偏移地址,var4 值(1)
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    // 這里用到了自旋鎖:一個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那么該執行緒將回圈等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出回圈,任何時刻最多只能有一個執行單元獲得鎖,
    do {
        // 獲取記憶體地址中的原物件的值
        var5 = this.getIntVolatile(var1, var2);
    
    // 借助CAS比較并交換,來實作getAndIncrement()方法的自增+1功能!
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

...
   
// 呼叫C++,執行比較并交換
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS : 比較當前作業記憶體中的值和主記憶體中的值,如果這個值是期望的,那么則執行操作!如果不是就

一直回圈!

CAS的ABA問題?

貍貓換太子

public class CasAbaTest {

    // CAS compareAndSet : 比較并交換!
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        /*
         * 類似于我們平時寫的SQL:樂觀鎖
         *
         * 如果某個執行緒在執行操作某個物件的時候,其他執行緒若操作了該物件,
         * 即使物件內容未發生變化,也需要告訴我,
         *
         * 期望、更新:
         * public final boolean compareAndSet(int expect, int update)
         * 如果我期望的值達到了,那么就更新,否則,就不更新,
         *									CAS 是CPU的并發原語!
         */

        // ============== 搗亂的執行緒 ==================
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());

        // ============== 期望的執行緒 ==================
        System.out.println(atomicInteger.compareAndSet(2020, 6666));
        System.out.println(atomicInteger.get());
    }
}

輸出結果:
true
2021
true
2020
true
6666 

上述案例中:假設我們期望的執行緒本來是需要將2020更換成6666,然而有一個搗亂的執行緒搶在期望執行緒之前執行,先把2020更換為了2021,然后又將2021更換回2020!

這樣看上去當期望執行緒執行時,初始值仍為2020沒有改變,但是實際上在搗亂執行緒中已經執行過2次更換操作了,而我們的期望執行緒并不知情!這就是ABA問題!

如何解決ABA問題?

本質上相當于采用樂觀鎖策略解決ABA問題!

public class CASDemo {
    /**
     * AtomicStampedReference 注意,
     * 如果泛型是一個包裝類,就需要注意物件的參考問題
     * 正常在業務操作,這里面比較的都是一個個物件
     */
    // 引數1:初始值100
    // 引數2:初始對應的版本號 initialStamp=1
    static AtomicStampedReference<Integer> atomicStampedReference =
            new AtomicStampedReference<>(100,1);

    // CAS compareAndSet : 比較并交換!
    public static void main(String[] args) {
        // 執行緒A:
        new Thread(()->{
            // 執行緒執行時,先獲得initialStamp版本號
            int stamp = atomicStampedReference.getStamp();

            System.out.println("A執行緒第1次拿到的版本號為:"+stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // cas比較并交換:100--->101
            atomicStampedReference.compareAndSet(
                    100,
                    101,
                    atomicStampedReference.getStamp(),// 獲得最新版本號
                    // 更新版本號
                    atomicStampedReference.getStamp() + 1);

            System.out.println("A執行緒第2次拿到的版本號為:"
                    +atomicStampedReference.getStamp());

            // cas比較并交換:101--->100
            System.out.println("A執行緒第2次是否執行了CAS:" +
                    atomicStampedReference.compareAndSet(
                            101,
                            100,
                            atomicStampedReference.getStamp(),
                            atomicStampedReference.getStamp() + 1));

            System.out.println("A執行緒第3次拿到的版本號為:"
                    +atomicStampedReference.getStamp());
        },"A").start();

        // 樂觀鎖的原理相同!
        // 執行緒B:
        new Thread(()->{
            // 獲得版本號
            int stamp = atomicStampedReference.getStamp();

            System.out.println("B執行緒第1次拿到的版本號為:"+stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // cas比較并交換:100--->99
            System.out.println("B執行緒第1次是否執行了CAS:" +
                    atomicStampedReference.compareAndSet(
                            100,
                            99,
                            stamp,
                            stamp + 1));

            System.out.println("B執行緒第2次拿到的版本號為:"
                    +atomicStampedReference.getStamp());
        },"B").start();
    }
}

這樣,在版本號initialStamp的限制下,每執行一次CAS,都會將版本號+1,這樣即使出現了 “貍貓換太子” 情況,期望執行緒也能及時知道!

輸出結果如下:

A執行緒第1次拿到的版本號為:1
B執行緒第1次拿到的版本號為:1
A執行緒第2次拿到的版本號為:2
A執行緒第2次是否執行了CAS:true
A執行緒第3次拿到的版本號為:3
B執行緒第1次是否執行了CAS:false
B執行緒第2次拿到的版本號為:3

總的來說,與MySQL的樂觀鎖表中加一個version欄位原理相同!

注意:

Integer 使用了物件快取機制,默認范圍是 -128 ~ 127 ,推薦使用靜態工廠方法 valueOf 獲取物件實體,而不是 new,因為 valueOf 使用快取,而 new 一定會創建新的物件分配新的記憶體空間;

下面是阿里巴巴開發手冊的規范點:

所以上面的案例,如果使用大于-128-127范圍的數字時候就會出現2個flase的情況!這里小伙伴一定要注意下~

16. 介紹下你對AQS的理解?

參考文章:AQS面試詳解

AQS的全稱為(AbstractQueuedSynchronizer)抽象的佇列式同步器是除了java自帶的synchronized關鍵字之外的鎖機制,這個類在java.util.concurrent.locks包下面,

它是一個Java提高的底層同步工具類,比如CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的!

實作了AQS的鎖有:自旋鎖、互斥鎖、讀鎖寫鎖、條件產量、信號量、柵欄都是AQS的衍生物!

17. ReentrantLock和synchronized的差別?

  • ReentrantLock和synchronized都是獨占鎖,可重入鎖,悲觀鎖
  • synchronized:
    • 1、java內置關鍵字
    • 2、無法判斷是否獲取鎖的狀態只能是非公平鎖
    • 3、加鎖解鎖的程序是隱式的,用戶不用手動操作,優點是操作簡單但顯得不夠靈活
    • 4、一般并發場景使用足夠、可以放在被遞回執行的方法上,且不用擔心執行緒最后能否正確釋放鎖
  • ReentrantLock:
    • 1、是個Lock介面的實作類
    • 2、可以判斷是否獲取到鎖,可以為公平鎖也可以是非公平鎖(默認)
    • 3、需要手動加鎖和解鎖,且解鎖的操作盡量要放在finally代碼塊中,保證執行緒正確釋放鎖
    • 5、創建的時候通過傳進引數true創建公平鎖,如果傳入的是false或沒傳引數則創建的是非公平鎖
    • 6、底層是AQS的state和FIFO佇列來控制加鎖

18. ReentrantReadWriteLock和ReentrantLock有什么區別?

ReentrantReadWriteLock

1、讀寫鎖介面ReadWriteLock介面的一個具體實作,實作了讀寫鎖分離

2、支持公平和非公平,底層也是基于AQS實作

3、允許從寫鎖降級為讀鎖
流程:先獲取寫鎖,然后獲取讀鎖,最后釋放寫鎖;但不能從讀鎖升級到寫鎖

4、重入:

  • 讀鎖后還可以獲取讀鎖;

  • 獲取了寫鎖之后既可以再次獲取寫鎖又可以獲取讀鎖

  • 讀鎖是共享的,寫鎖是獨占的!讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,主要是提升了讀寫的性能 !

ReentrantLock是獨占鎖且可重入的,相比synchronized而言功能更加豐富也更適合復雜的并發場景,但是也有弊端,假如有兩個執行緒A/B訪問資料,加鎖是為了防止執行緒A在寫資料, 執行緒B在讀資料造成的資料不一致; 但執行緒A在讀資料,執行緒C也在讀資料,讀資料是不會改變資料沒有必要加鎖,但是ReentrantLock還是加鎖了,降低了程式的性能,所以就有了ReadWriteLock讀寫鎖介面!

19. 是否了解阻塞佇列BlockingQueue?

BlockingQueue阻塞佇列

  • ArrayBlockingQueue,ArrayBlockingQueue
  • put方法用來向隊尾存入元素,如果佇列滿,則阻塞
  • take方法用來從隊首取元素,如果佇列為空,則阻塞

BlockingQueue: juc包下的提供了執行緒安全的佇列訪問的介面,并發包下很多高級同步類的實作都是基于阻塞佇列實作的!

  • 1、當阻塞佇列進行插入資料時,如果佇列已滿,執行緒將會阻塞等待直到佇列非滿
  • 2、從阻塞佇列讀資料時,如果佇列為空,執行緒將會阻塞等待直到佇列里面是非空的時候

常見的阻塞佇列

  • ArrayBlockingQueue:
    • 基于陣列實作的一個阻塞佇列,需要指定容量大小,FIFO先進先出順序;
  • LinkedBlockingQueue:
    • 基于鏈表實作的一個阻塞佇列,如果不指定容量大小,默認Integer.MAX_VALUE,FIFO先進先出順序;
  • PriorityBlockingQueue:
    • 一個支持優先級的無界阻塞佇列,默認情況下元素采用自然順序升序排序,也可以自定義排序實作java.lang.Comparable介面;
  • DelayQueue:
    • 延遲佇列在指定時間才能獲取佇列元素的功能佇列頭元素是最接近過期的元素,里面的物件必須實作 java.util.concurrent.Delayed 介面并實作CompareTo和getDelay方法;

?

擴展:你知道非阻塞佇列ConcurrentLinkedQueue嗎,它怎么實作執行緒安全的?

參考文章:Java并發編程之ConcurrentLinkedQueue詳解

20. java里有哪些是常用的執行緒池?

使用執行緒池的好處:

重用存在的執行緒,減少物件創建銷毀的開銷,有效的控制最大并發執行緒數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞,且可以定時定期執行、單執行緒、并發數控制,配置任務過多任務后的拒絕策略等功能

類別:

  • newFixedThreadPool :
    • 一個定長執行緒池,可控制執行緒最大并發數
  • newCachedThreadPool:
    • 一個可快取執行緒池
  • newSingleThreadExecutor:
    • 一個單執行緒化的執行緒池,用唯一的作業執行緒來執行任務
  • newScheduledThreadPool:
    • 一個定長執行緒池,支持定時/周期性任務執行

【阿里巴巴編碼規范】 執行緒池不允許使用 Executors 去創建,要通過 ThreadPoolExecutor的方式原因?

	Executors創建的執行緒池底層也是呼叫 ThreadPoolExecutor,只不過使用不同的引數、佇列、拒絕策略等
	如果使用不當,會造成資源耗盡問題

	直接使用ThreadPoolExecutor讓使用者更加清楚執行緒池允許規則,常見引數的使用,避免風險

##### 常見的執行緒池問題:
    newFixedThreadPool和newSingleThreadExecutor: 
    佇列使用LinkedBlockingQueue,佇列長度為 Integer.MAX_VALUE,可能造成堆積,導致OOM

    newScheduledThreadPool和newCachedThreadPool:
    執行緒池里面允許最大的執行緒數是Integer.MAX_VALUE,可能會創建過多執行緒,導致OOM

ThreadPoolExecutor建構式里面的引數,能否解釋下各個引數的作用?

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize核心執行緒數,執行緒池也會維護執行緒的最少數量,默認情況下核心執行緒會一直存活,即使沒有任務也不會受存keepAliveTime控制!
    :在剛創建執行緒池時執行緒不會立即啟動,到有任務提交時才開始創建執行緒并逐步執行緒數目達到corePoolSize

  • maximumPoolSize執行緒池維護執行緒的最大數量,超過將被阻塞!
    :當核心執行緒滿,且阻塞佇列也滿時,才會判斷當前執行緒數是否小于最大執行緒數,才決定是否創建新執行緒

  • keepAliveTime非核心執行緒的閑置超時時間,超過這個時間就會被回收,直到執行緒數量等于corePoolSize

  • unit:指定keepAliveTime的單位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS

  • workQueue執行緒池中的任務佇列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue

  • threadFactory創建新執行緒時使用的工廠

  • handler:RejectedExecutionHandler是一個介面且只有一個方法,執行緒池中的數量大于maximumPoolSize,對拒絕任務的處理策略,默認有4種策略:

    • AbortPolicy
    • CallerRunsPolicy
    • DiscardOldestPolicy
    • DiscardPolicy

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/262189.html

標籤:java

上一篇:遞增順序二叉查找樹(樹的中序遍歷)Leetcode 刷題日記 2021.2.20

下一篇:JVM類的加載機制

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more