天天看點

面試筆記整理(二)

一、Java基礎

1.1.1 Java并發包

  • Java有幾種加鎖方式分别是什麼 什麼實作原理
    • 全文位址:https://tech.meituan.com/2018/11/15/java-lock.html
    • 答案:
      • 樂觀鎖 悲觀鎖
      synchronized關鍵字和Lock的實作類都是悲觀鎖。
      樂觀鎖在Java中是通過使用無鎖程式設計來實作,最常采用的是CAS算法,Java原子類中的遞增操作就通過CAS自旋實作的。
      
      悲觀鎖适合寫操作多的場景,先加鎖可以保證寫操作時資料正确。
      樂觀鎖适合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
      
      CAS雖然很高效,但是它也存在三大問題,這裡也簡單說一下:
      1.ABA問題。 ABA問題的解決思路就是在變量前面添加版本号,每次變量更新的時候都把版本号加一,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。
      2.循環時間長開銷大。CAS操作如果長時間不成功,會導緻其一直自旋,給CPU帶來非常大的開銷。
      3.隻能保證一個共享變量的原子操作。對一個共享變量執行操作時,CAS能夠保證原子操作,但是對多個共享變量操作時,CAS是無法保證操作的原子性的。
                 
      • 自旋鎖 适應性自旋鎖
      自旋鎖的實作原理同樣也是CAS,AtomicInteger中調用unsafe進行自增操作的源碼中的do-while循環就是一個自旋操作,如果修改數值失敗則通過循環來執行自旋,直至修改成功。
      
      自适應意味着自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀态來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運作中,那麼虛拟機就會認為這次自旋也是很有可能再次成功,進而它将允許自旋等待持續相對更長的時間。如果對于某個鎖,自旋很少成功獲得過,那在以後嘗試擷取這個鎖時将可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。
      
      在自旋鎖中 另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock
                 
      • 無鎖 偏向鎖 輕量級鎖 重量級鎖
      鎖狀态             存儲内容                                               Mark Word内容
      無鎖               對象的hashCode、對象分代年齡、是否是偏向鎖(0)           01
      偏向鎖             偏向線程ID、偏向時間戳、對象分代年齡、是否是偏向鎖(1)     01
      輕量級鎖           指向棧中鎖記錄的指針	                                 00
      重量級鎖           指向互斥量(重量級鎖)的指針                             10
      
      無鎖:CAS原理及應用即是無鎖的實作。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的。
      偏向鎖:在大多數情況下,鎖總是由同一線程多次獲得,不存在多線程競争,是以出現了偏向鎖。其目标就是在隻有一個線程執行同步代碼塊時能夠提高性能。
      輕量級鎖:未獲得鎖的其他線程會通過自旋的形式嘗試擷取鎖,不會阻塞,進而提高性能。
      重量級鎖:未擷取到鎖的線程進入阻塞狀态。
                 
      • 偏向鎖通過對比Mark Word解決加鎖問題,避免執行CAS操作。而輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。重量級鎖是将除了擁有鎖的線程以外的線程都阻塞。
      面試筆記整理(二)
      • 公平鎖 非公平鎖
        • 公平鎖是指多個線程按照申請鎖的順序來擷取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
        • 非公平鎖是多個線程加鎖時直接嘗試擷取鎖,擷取不到才會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個線程可以無需阻塞直接擷取到鎖,是以非公平鎖有可能出現後申請鎖的線程先擷取鎖的場景。非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處于等待隊列中的線程可能會餓死,或者等很久才會獲得鎖。
        面試筆記整理(二)
        面試筆記整理(二)
      • 可重入鎖 非可重入鎖
        • 可重入鎖又名遞歸鎖,是指在同一個線程在外層方法擷取鎖的時候,再進入該線程的内層方法會自動擷取鎖(前提鎖對象得是同一個對象或者class),不會因為之前已經擷取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。下面用示例代碼來進行分析:
          public class Widget {
              public synchronized void doSomething() {
                  System.out.println("方法1執行...");
                  doOthers();
              }
          
              public synchronized void doOthers() {
                  System.out.println("方法2執行...");
              }
          }
          
          /* 
          在上面的代碼中,類中的兩個方法都是被内置鎖synchronized修飾的,doSomething()方法中調用doOthers()方法。因為内置鎖是可重入的,是以同一個線程在調用doOthers()時可以直接獲得目前對象的鎖,進入doOthers()進行操作。
          
          如果是一個不可重入鎖,那麼目前線程在調用doOthers()之前需要将執行doSomething()時擷取目前對象的鎖釋放掉,實際上該對象鎖已被目前線程所持有,且無法釋放。是以此時會出現死鎖。
          */
                     
          面試筆記整理(二)
      • 共享鎖 排他鎖
        • 獨享鎖也叫排他鎖,是指該鎖一次隻能被一個線程所持有。如果線程T對資料A加上排它鎖後,則其他線程不能再對A加任何類型的鎖。獲得排它鎖的線程即能讀資料又能修改資料。JDK中的synchronized和JUC中Lock的實作類就是互斥鎖。
        • 共享鎖是指該鎖可被多個線程所持有。如果線程T對資料A加上共享鎖後,則其他線程隻能對A再加共享鎖,不能加排它鎖。獲得共享鎖的線程隻能讀資料,不能修改資料。
        • 獨享鎖與共享鎖也是通過AQS來實作的,通過實作不同的方法,來實作獨享或者共享。
          面試筆記整理(二)
        • 在ReentrantReadWriteLock裡面,讀鎖和寫鎖的鎖主體都是Sync,但讀鎖和寫鎖的加鎖方式不一樣。**讀鎖是共享鎖,寫鎖是獨享鎖。**讀鎖的共享鎖可保證并發讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因為讀鎖和寫鎖是分離的。是以ReentrantReadWriteLock的并發性相比一般的互斥鎖有了很大提升。
        • AQS的state字段,獨享鎖中這個值通常是0或者1(如果是重入鎖的話state值就是重入的次數),在共享鎖中state就是持有鎖的數量。但是在ReentrantReadWriteLock中有讀、寫兩把鎖,是以需要在一個整型變量state上分别描述讀鎖和寫鎖的數量(或者也可以叫狀态)。于是将state變量“按位切割”切分成了兩個部分,高16位表示讀鎖狀态(讀鎖個數),低16位表示寫鎖狀态(寫鎖個數)。如下圖所示:
          面試筆記整理(二)
        • 寫鎖源碼:
          protected final boolean tryAcquire(int acquires) {
          	Thread current = Thread.currentThread();
          	int c = getState(); // 取到目前鎖的個數
          	int w = exclusiveCount(c); // 取寫鎖的個數w
          	if (c != 0) { // 如果已經有線程持有了鎖(c!=0)
          		if (w == 0 || current != getExclusiveOwnerThread()) // 如果寫線程數(w)為0(換言之存在讀鎖) 或者持有鎖的線程不是目前線程就傳回失敗
          			return false;
          		if (w + exclusiveCount(acquires) > MAX_COUNT)    // 如果寫入鎖的數量大于最大數(65535,2的16次方-1)就抛出一個Error。
                		throw new Error("Maximum lock count exceeded");
              // Reentrant acquire
              setState(c + acquires);
              return true;
            }
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果當且寫線程數為0,并且目前線程需要阻塞那麼就傳回失敗;或者如果通過CAS增加寫線程數失敗也傳回失敗。
          		return false;
            setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),則設定目前線程或鎖的擁有者
             return true;
          }
          
          /*
          隻有等待其他讀線程都釋放了讀鎖,寫鎖才能被目前線程擷取,而寫鎖一旦被擷取,則其他讀寫線程的後續通路均被阻塞。寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀态,當寫狀态為0時表示寫鎖已被釋放,然後等待的讀寫線程才能夠繼續通路讀寫鎖,同時前次寫線程的修改對後續的讀寫線程可見。
          */
                     
        • 讀鎖源碼:
          protected final int tryAcquireShared(int unused) {
              Thread current = Thread.currentThread();
              int c = getState();
              if (exclusiveCount(c) != 0 &&
                  getExclusiveOwnerThread() != current)
                  return -1;                                   // 如果其他線程已經擷取了寫鎖,則目前線程擷取讀鎖失敗,進入等待狀态
              int r = sharedCount(c);
              if (!readerShouldBlock() &&
                  r < MAX_COUNT &&
                  compareAndSetState(c, c + SHARED_UNIT)) {
                  if (r == 0) {
                      firstReader = current;
                      firstReaderHoldCount = 1;
                  } else if (firstReader == current) {
                      firstReaderHoldCount++;
                  } else {
                      HoldCounter rh = cachedHoldCounter;
                      if (rh == null || rh.tid != getThreadId(current))
                          cachedHoldCounter = rh = readHolds.get();
                      else if (rh.count == 0)
                          readHolds.set(rh);
                      rh.count++;
                  }
                  return 1;
              }
              return fullTryAcquireShared(current);
          }
          
          /**
          如果其他線程已經擷取了寫鎖,則目前線程擷取讀鎖失敗,進入等待狀态。如果目前線程擷取了寫鎖或者寫鎖未被擷取,則目前線程(線程安全,依靠CAS保證)增加讀狀态,成功擷取讀鎖。讀鎖的每次釋放(線程安全的,可能有多個讀線程同時釋放讀鎖)均減少讀狀态,減少的值是“1<<16”。是以讀寫鎖才能實作讀讀的過程共享,而讀寫、寫讀、寫寫的過程互斥。
          */
                     
  • Java建立線程池的接口需要哪些參數
    • new ThreadPoolExecutor()自定義建立public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) ;
    • corePoolSize:核心池的大小,在建立了線程池後,預設情況下,線程池中并沒有任何線程,而是等待有任務到來才建立線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預建立線程的意思,即在沒有任務到來之前就建立corePoolSize個線程或者一個線程。預設情況下,在建立了線程池後,線程池中的線程數為0,當有任務來之後,就會建立一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中。
    • maximumPoolSize:線程池最大線程數,這個參數也是一個非常重要的參數,它表示線上程池中最多能建立多少個線程。
    • keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。
    • unit:參數keepAliveTime的時間機關。
      TimeUnit.DAYS;              //天
      TimeUnit.HOURS;             //小時
      TimeUnit.MINUTES;           //分鐘
      TimeUnit.SECONDS;           //秒
      TimeUnit.MILLISECONDS;      //毫秒
      TimeUnit.MICROSECONDS;      //微妙
      TimeUnit.NANOSECONDS;       //納秒
                 
    • workQueue:一個阻塞隊列,用來存儲等待執行的任務。
      ArrayBlockingQueue:是一個用數組實作的有界阻塞隊列,此隊列按照先進先出(FIFO)的原則對元素進行排序。
      LinkedBlockingQueue:一個由連結清單結構組成的有界隊列,此隊列的長度為Integer.MAX_VALUE。此隊列按照先進先出的順序進行排序。
      PriorityBlockingQueue: 一個支援線程優先級排序的無界隊列,預設自然序進行排序,也可以自定義實作compareTo方法來指定元素排序規則,不能保證同優先級元素的順序。
      SynchronousQueue: 一個不存儲元素的阻塞隊列,每一個put操作必須等待take操作,否則不能添加元素。支援公平鎖和非公平鎖。
      PriorityBlockingQueue:一個支援線程優先級排序的無界隊列,預設自然序進行排序,也可以自定義實作compareTo方法來指定元素排序規則,不能保證同優先級元素的順序。
      /** 
      ArrayBlockingQueue和PriorityBlockingQueue使用較少,
      一般使用LinkedBlockingQueue和SynchronousQueue。線程池的排隊政策與BlockingQueue有關。
      */
                 
    • threadFactory:于設定建立線程的工廠,可以通過線程工廠給每個建立出來的線程做些更有意義的事情,比如設定daemon和優先級等等。
    • handler:表示當拒絕處理任務時的政策
      1、AbortPolicy:直接抛出異常。
      2、CallerRunsPolicy:隻用調用者所線上程來運作任務。
      3、DiscardOldestPolicy:丢棄隊列裡最近的一個任務,并執行目前任務。
      4、DiscardPolicy:不處理,丢棄掉。
      5、也可以根據應用場景需要來實作RejectedExecutionHandler接口自定義政策。如記錄日志或持久化不能處理的任務。
                 

1.1.2 Java的IO

  • Java的IO都有哪些 他們有什麼差別
  • NIO相關及優劣勢
  • 阻塞非阻塞 異步同步的概念

二、Spring相關

2.1.1 Spring事務

  • Spring事務是怎麼實作的
    • **答案:**動态代理
  • Spring事務的四種特性
    • 答案:
      原子性 (atomicity):強調事務的不可分割.
      一緻性 (consistency):事務的執行的前後資料的完整性保持一緻.
      隔離性 (isolation):一個事務執行的過程中,不應該受到其他事務的幹擾
      持久性(durability) :事務一旦結束,資料就持久到資料庫
                 
  • Spring事務的隔離級别
    • 答案:
      DEFAULT 這是一個PlatfromTransactionManager預設的隔離級别,使用資料庫預設的事務隔離級别.
      未送出讀(read uncommited) :髒讀,不可重複讀,虛讀都有可能發生
      已送出讀 (read commited):避免髒讀。但是不可重複讀和虛讀有可能發生
      可重複讀 (repeatable read) :避免髒讀和不可重複讀.但是虛讀有可能發生.
      串行化的 (serializable) :避免以上所有讀問題.
      Mysql 預設:可重複讀
      Oracle 預設:已送出讀
                 
  • Spring事務的傳播機制
    • 答案:
      保證同一個事務中
      PROPAGATION_REQUIRED 支援目前事務,如果不存在 就建立一個(預設)
      PROPAGATION_SUPPORTS 支援目前事務,如果不存在,就不使用事務
      PROPAGATION_MANDATORY 支援目前事務,如果不存在,抛出異常
      保證沒有在同一個事務中
      PROPAGATION_REQUIRES_NEW 如果有事務存在,挂起目前事務,建立一個新的事務
      PROPAGATION_NOT_SUPPORTED 以非事務方式運作,如果有事務存在,挂起目前事務
      PROPAGATION_NEVER 以非事務方式運作,如果有事務存在,抛出異常
      PROPAGATION_NESTED 如果目前事務存在,則嵌套事務執行
                 
  • Spring事務在什麼情況下失效
    • A方法調用B B方法是事務的 A方法不是事務的 這時候事務生效嗎
    • 答案: 不生效
      分為不同情況 如果是自調用 那是不生效的(不走代理方法) 如果是外部調用則生效
      
      是以解決方案:
      1. 改成外部調用
         再聲明一個 Service,把更新表的邏輯放過去。
      2. 使用程式設計式事務
         transactionTemplate.execute()
      3. 調外部方法,然後外部方法再我調回來
         TransactionalComponent.required()
                 

三、 JVM相關

3.1.1 記憶體結構

  • 都有哪些 簡單介紹
  • 對象什麼時候從新生代過度到老年代

3.1.2 垃圾回收器

  • 垃圾回收器都有哪些
  • 垃圾回收算法

四、 Mysql相關

4.1.1 索引

  • Mysql索引原理
    • B+數
  • 索引在什麼情況下不生效
    • 答案:
      1. 組合索引未使用最左字首,例如組合索引(A,B),where B=b不會使用索引;
      2. like未使用最左字首,where A like ‘%China’;
      3. 搜尋一個索引而在另一個索引上做order by,where A=a order by B,隻使用A上的索引,因為查詢隻使用一個索引 ;
      4. or會使索引失效。如果查詢字段相同,也可以使用索引。例如where A=a1 or A=a2(生效),where A=a or B=b(失效)
      5. 如果列類型是字元串,要使用引号。例如where A=‘China’,否則索引失效(會進行類型轉換);
      6. 在索引列上的操作,函數(upper()等)、or、!=(<>)、not in等;
  • 聚簇索引相關
    • 答案:
      • 聚簇索引是實體索引,資料表就是按順序存儲的,實體上是連續的。
      • 一旦建立了聚簇索引,表中的所有列都根據聚簇索引的key來存儲。
      • 因為聚簇索引是按該列的排序存儲的,是以一個表隻能有一個聚簇索引。
        面試筆記整理(二)
        • 根據上文可以知主鍵為聚簇索引,實體存儲是根據ID的增加排序遞增連續存儲的。
        • 普通索引K也是B+Tree的資料結構(請看右圖),但是它不是聚簇索引,是以為非聚簇索引或者輔助索引(聚簇索引隻可能是主鍵,或者所有組成唯一鍵的所有列都為NOT NULL的第一個唯一索引,或者隐式建立的聚簇索引這三種情況)。

4.1.2 事務相關

  • Mysql的事務隔離級别
    • 答案:

      1、髒讀:事務A讀取了事務B更新的資料,然後B復原操作,那麼A讀取到的資料是髒資料

      2、不可重複讀:事務 A 多次讀取同一資料,事務 B 在事務A多次讀取的過程中,對資料作了更新并送出,導緻事務A多次讀取同一資料時,結果 不一緻。

      3、幻讀:系統管理者A将資料庫中所有學生的成績從具體分數改為ABCDE等級,但是系統管理者B就在這個時候插入了一條具體分數的記錄,當系統管理者A改結束後發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。

      小結:不可重複讀的和幻讀很容易混淆,不可重複讀側重于修改,幻讀側重于新增或删除。解決不可重複讀的問題隻需鎖住滿足條件的行,解決幻讀需要鎖表

      事務隔離級别 髒讀 不可重複讀 幻讀
      讀未送出(read-uncommitted)
      不可重複讀(read-committed)
      可重複讀(repeatable-read)
      串行化(serializable)
      • mysql預設的事務隔離級别為repeatable-read

五、 Mybatis相關

5.1.1 Mybatis緩存

  • 一級緩存和二級緩存的差別
  • 答案:
    • 一級緩存
    1.MyBatis一級緩存的生命周期和SqlSession一緻。
    2.MyBatis一級緩存内部設計簡單,隻是一個沒有容量限定的HashMap,在緩存的功能性上有所欠缺。
    3.MyBatis的一級緩存最大範圍是SqlSession内部,有多個SqlSession或者分布式的環境下,資料庫寫操作會引起髒資料,建議設定緩存級别為Statement。
               
    • 二級緩存
    1.MyBatis的二級緩存相對于一級緩存來說,實作了SqlSession之間緩存資料的共享,同時粒度更加的細,能夠到namespace級别,通過Cache接口實作類不同的組合,對Cache的可控性也更強。
    2.MyBatis在多表查詢時,極大可能會出現髒資料,有設計上的缺陷,安全使用二級緩存的條件比較苛刻。
    3.在分布式環境下,由于預設的MyBatis Cache實作都是基于本地的,分布式環境下必然會出現讀取到髒資料,需要使用集中式緩存将MyBatis的Cache接口實作,有一定的開發成本,直接使用Redis、Memcached等分布式緩存可能成本更低,安全性也更高。
               

繼續閱讀