一、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()時擷取目前對象的鎖釋放掉,實際上該對象鎖已被目前線程所持有,且無法釋放。是以此時會出現死鎖。 */
面試筆記整理(二)
- 可重入鎖又名遞歸鎖,是指在同一個線程在外層方法擷取鎖的時候,再進入該線程的内層方法會自動擷取鎖(前提鎖對象得是同一個對象或者class),不會因為之前已經擷取過還沒釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。下面用示例代碼來進行分析:
- 共享鎖 排他鎖
- 獨享鎖也叫排他鎖,是指該鎖一次隻能被一個線程所持有。如果線程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+數
- 索引在什麼情況下不生效
- 答案:
- 組合索引未使用最左字首,例如組合索引(A,B),where B=b不會使用索引;
- like未使用最左字首,where A like ‘%China’;
- 搜尋一個索引而在另一個索引上做order by,where A=a order by B,隻使用A上的索引,因為查詢隻使用一個索引 ;
- or會使索引失效。如果查詢字段相同,也可以使用索引。例如where A=a1 or A=a2(生效),where A=a or B=b(失效)
- 如果列類型是字元串,要使用引号。例如where A=‘China’,否則索引失效(會進行類型轉換);
- 在索引列上的操作,函數(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等分布式緩存可能成本更低,安全性也更高。