Java
1. 多個線程同時讀寫,讀線程的數量遠大於寫線程,你認為應該如何解決並發的問題?你會選擇加什么樣的鎖?
a.數據庫讀寫分離;
b.一種是代碼層次上的,如java中的同步鎖,典型的就是同步關鍵字synchronized;
樂觀鎖,(不推薦悲觀鎖),服務層面可以采取負載均衡;負載均衡有幾種實現方式(HTTP客戶端 Feign.以及nginx),緩存
c.非線程安全類
public class Counter { private int count; public int getCount(){ return count++; } }
線程安全類
public class Counter { private int count; AtomicInteger atomicCount = new AtomicInteger( 0 ); /**1、synchronized 所以線程安全*/ public synchronized int getCount(){ return count++; } /** 2、原子增長的操作,所以線程安全 */ public int getCountAtomically(){ return atomicCount.incrementAndGet(); } }
2. JAVA的AQS是否了了解,它是干嘛的?
提到JAVA加鎖,我們通常會想到synchronized關鍵字或者是Java Concurrent Util(后面簡稱JCU)包下面的Lock,今天就來扒一扒Lock是如何實現的,比如我們可以先提出一些問題:當我們通過實例化一個ReentrantLock並且調用它的lock或unlock的時候,這其中發生了什么?如果多個線程同時對同一個鎖實例進行lock或unlcok操作,這其中又發生了什么?
**什么是可重入鎖?**
ReentrantLock是可重入鎖,什么是可重入鎖呢?**可重入鎖就是當前持有該鎖的線程能夠多次獲取該鎖,無需等待。**可重入鎖是如何實現的呢?這要從ReentrantLock的一個內部類Sync的父類說起,Sync的父類是AbstractQueuedSynchronizer(后面簡稱AQS)。
**什么是AQS?**
AQS是JDK1.5提供的一個基於FIFO等待隊列實現的一個用於實現同步器的基礎框架,這個基礎框架的重要性可以這么說,JCU包里面幾乎所有的有關鎖、多線程並發以及線程同步器等重要組件的實現都是基於AQS這個框架。**AQS的核心思想是基於volatile int state這樣的一個屬性同時配合Unsafe工具對其原子性的操作來實現對當前鎖的狀態進行修改。**當state的值為0的時候,標識改Lock不被任何線程所占有。
**ReentrantLock鎖的架構**
ReentrantLoc的架構相對簡單,主要包括一個Sync的內部抽象類以及Sync抽象類的兩個實現類。上面已經說過了Sync繼承自AQS,他們的結構示意圖如下:
上圖除了AQS之外,我把AQS的父類AbstractOwnableSynchronizer(后面簡稱AOS)也畫了進來,可以稍微提一下,AOS主要提供一個exclusiveOwnerThread屬性,用於關聯當前持有該鎖的線程。另外、Sync的兩個實現類分別是NonfairSync和FairSync,由名字大概可以猜到,一個是用於實現公平鎖、一個是用於實現非公平鎖。那么Sync為什么要被設計成內部類呢?我們可以看看AQS主要提供了哪些protect的方法用於修改state的狀態,我們發現Sync被設計成為安全的外部不可訪問的內部類。ReentrantLock中所有涉及對AQS的訪問都要經過Sync,其實,Sync被設計成為內部類主要是為了安全性考慮,這也是作者在AQS的comments上強調的一點。
**AQS的等待隊列**
作為AQS的核心實現的一部分,舉個例子來描述一下這個隊列長什么樣子,我們假設目前有三個線程Thread1、Thread2、Thread3同時去競爭鎖,如果結果是Thread1獲取了鎖,Thread2和Thread3進入了等待隊列,那么他們的樣子如下:
AQS的等待隊列基於一個雙向鏈表實現的,HEAD節點不關聯線程,后面兩個節點分別關聯Thread2和Thread3,他們將會按照先后順序被串聯在這個隊列上。這個時候如果后面再有線程進來的話將會被當做隊列的TAIL。
**1)入隊列**
我們來看看,當這三個線程同時去競爭鎖的時候發生了什么?
代碼:
public final void acquire(int arg) { if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
三個線程同時進來,他們會首先會通過CAS去修改state的狀態,如果修改成功,那么競爭成功,因此這個時候三個線程只有一個CAS成功,其他兩個線程失敗,也就是tryAcquire返回false。接下來,addWaiter會把將當前線程關聯的EXCLUSIVE類型的節點入隊列
代碼:
privateNode addWaiter(Node mode) { Node node = newNode(Thread.currentThread(), mode); Node pred = tail; if(pred != null) { node.prev = pred; if(compareAndSetTail(pred, node)) { pred.next = node; returnnode; } } enq(node); returnnode; }
解讀:
如果隊尾節點不為null,則說明隊列中已經有線程在等待了,那么直接入隊尾。對於我們舉的例子,這邊的邏輯應該是走enq,也就是開始隊尾是null,其實這個時候整個隊列都是null的
代碼:
privateNode enq(finalNode node) { for(;;) { Node t = tail; if(t == null) { // Must initialize if(compareAndSetHead(newNode())) tail = head; } else{ node.prev = t; if(compareAndSetTail(t, node)) { t.next = node; returnt; } } } }
解讀:
如果Thread2和Thread3同時進入了enq,同時t==null,則進行CAS操作對隊列進行初始化,這個時候只有一個線程能夠成功,然后他們繼續進入循環,第二次都進入了else代碼塊,這個時候又要進行CAS操作,將自己放在隊尾,因此這個時候又是只有一個線程成功,我們假設是Thread2成功,哈哈,Thread2開心的返回了,Thread3失落的再進行下一次的循環,最終入隊列成功,返回自己。
**2)並發問題**
基於上面兩段代碼,**他們是如何實現不進行加鎖,當有多個線程,或者說很多很多的線程同時執行的時候,怎么能保證最終他們都能夠乖乖的入隊列而不會出現並發問題的呢?**這也是這部分代碼的經典之處,**多線程競爭,熱點、單點在隊列尾部,多個線程都通過【CAS+死循環】這個free-lock黃金搭檔來對隊列進行修改,每次能夠保證只有一個成功,如果失敗下次重試,如果是N個線程,那么每個線程最多loop N次,最終都能夠成功。**
**3)掛起等待線程**
上面只是addWaiter的實現部分,那么節點入隊列之后會繼續發生什么呢?那就要看看acquireQueued是怎么實現的了,為保證文章整潔,代碼我就不貼了,同志們自行查閱,我們還是以上面的例子來看看,Thread2和Thread3已經被放入隊列了,進入acquireQueued之后:
1. 對於Thread2來說,它的prev指向HEAD,因此會首先再嘗試獲取鎖一次,如果失敗,則會將HEAD的waitStatus值為SIGNAL,下次循環的時候再去嘗試獲取鎖,如果還是失敗,且這個時候prev節點的waitStatus已經是SIGNAL,則這個時候線程會被通過LockSupport掛起。
2. 對於Thread3來說,它的prev指向Thread2,因此直接看看Thread2對應的節點的waitStatus是否為SIGNAL,如果不是則將它設置為SIGNAL,再給自己一次去看看自己有沒有資格獲取鎖,如果Thread2還是擋在前面,且它的waitStatus是SIGNAL,則將自己掛起。
如果Thread1死死的握住鎖不放,那么Thread2和Thread3現在的狀態就是掛起狀態啦,而且HEAD,以及Thread的waitStatus都是SIGNAL,盡管他們在整個過程中曾經數次去嘗試獲取鎖,但是都失敗了,失敗了不能死循環呀,所以就被掛起了。當前狀態如下:
**鎖釋放-等待線程喚起**
我們來看看當Thread1這個時候終於做完了事情,調用了unlock准備釋放鎖,這個時候發生了什么。
代碼:
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
解讀:
首先,Thread1會修改AQS的state狀態,加入之前是1,則變為0,注意這個時候對於非公平鎖來說是個很好的插入機會,舉個例子,如果鎖是公平鎖,這個時候來了Thread4,那么這個鎖將會被Thread4搶去。。。
我們繼續走常規路線來分析,當Thread1修改完狀態了,判斷隊列是否為null,以及隊頭的waitStatus是否為0,如果waitStatus為0,說明隊列無等待線程,按照我們的例子來說,隊頭的waitStatus為SIGNAL=-1,因此這個時候要通知隊列的等待線程,可以來拿鎖啦,這也是unparkSuccessor做的事情,unparkSuccessor主要做三件事情:
1. 將隊頭的waitStatus設置為0.
2. 通過從隊列尾部向隊列頭部移動,找到最后一個waitStatus<=0的那個節點,也就是離隊頭最近的沒有被cancelled的那個節點,隊頭這個時候指向這個節點。
3. 將這個節點喚醒,其實這個時候Thread1已經出隊列了。
還記得線程在哪里掛起的么,上面說過了,在acquireQueued里面,我沒有貼代碼,自己去看哦。這里我們也大概能理解AQS的這個隊列為什么叫FIFO隊列了,因此每次喚醒僅僅喚醒隊頭等待線程,讓隊頭等待線程先出。
**羊群效應**
這里說一下羊群效應,當有多個線程去競爭同一個鎖的時候,假設鎖被某個線程占用,那么如果有成千上萬個線程在等待鎖,有一種做法是同時喚醒這成千上萬個線程去去競爭鎖,這個時候就發生了羊群效應,海量的競爭必然造成資源的劇增和浪費,因此終究只能有一個線程競爭成功,其他線程還是要老老實實的回去等待。**AQS的FIFO的等待隊列給解決在鎖競爭方面的羊群效應問題提供了一個思路:保持一個FIFO隊列,隊列每個節點只關心其前一個節點的狀態,線程喚醒也只喚醒隊頭等待線程。**其實這個思路已經被應用到了分布式鎖的實踐中,見:Zookeeper分布式鎖的改進實現方案。
**總結**
這篇文章粗略的介紹一下ReentrantLock以及鎖實現基礎框架AQS的實現原理,大致上通過舉了個三個線程競爭鎖的例子,從lock、unlock過程發生了什么這個問題,深入了解AQS基於狀態的標識以及FIFO等待隊列方面的工作原理,最后擴展介紹了一下羊群效應問題,博主才疏學淺,還請多多指教。
3. 除了synchronized關鍵字之外,你是怎么來保障線程安全的?
見上題!
4. 什么時候需要加volatile關鍵字?它能保證線程安全嗎?
對於volatile這個關鍵字,相信很多朋友都聽說過,甚至使用過,這個關鍵字雖然字面上理解起來比較簡單,但是要用好起來卻不是一件容易的事。
這篇文章將從多個方面來講解volatile,讓你對它更加理解。
計算機中為什么會出現線程不安全的問題
volatile既然是與線程安全有關的問題,那我們先來了解一下計算機在處理數據的過程中為什么會出現線程不安全的問題。
大家都知道,計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中會涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。
為了處理這個問題,在CPU里面就有了高速緩存(Cache)的概念。當程序在運行過程中,會將運算需要的數據從主存復制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之后,再將高速緩存中的數據刷新到主存當中。
我舉個簡單的例子,比如cpu在執行下面這段代碼的時候,
t = t + 1;
會先從高速緩存中查看是否有t的值,如果有,則直接拿來使用,如果沒有,則會從主存中讀取,讀取之后會復制一份存放在高速緩存中方便下次使用。之后cup進行對t加1操作,然后把數據寫入高速緩存,最后會把高速緩存中的數據刷新到主存中。這一過程在單線程運行是沒有問題的,但是在多線程中運行就會有問題了。在多核CPU中,每條線程可能運行於不同的CPU中,因此每個線程運行時有自己的高速緩存(對單核CPU來說,其實也會出現這種問題,只不過是以線程調度的形式來分別執行的,本次講解以多核cup為主)。這時就會出現同一個變量在兩個高速緩存中的值不一致問題了。例如:
兩個線程分別讀取了t的值,假設此時t的值為0,並且把t的值存到了各自的高速緩存中,然后線程1對t進行了加1操作,此時t的值為1,並且把t的值寫回到主存中。但是線程2中高速緩存的值還是0,進行加1操作之后,t的值還是為1,然后再把t的值寫回主存。
此時,就出現了線程不安全問題了。
Java中的線程安全問題
上面那種線程安全問題,可能對於不同的操作系統會有不同的處理機制,例如Windows操作系統和Linux的操作系統的處理方法可能會不同。
我們都知道,Java是一種誇平台的語言,因此Java這種語言在處理線程安全問題的時候,會有自己的處理機制,例如volatile關鍵字,synchronized關鍵字,並且這種機制適用於各種平台。
Java內存模型規定所有的變量都是存在主存當中(類似於前面說的物理內存),每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。
由於java中的每個線程有自己的工作空間,這種工作空間相當於上面所說的高速緩存,因此多個線程在處理一個共享變量的時候,就會出現線程安全問題。
這里簡單解釋下共享變量,上面我們所說的t就是一個共享變量,也就是說,能夠被多個線程訪問到的變量,我們稱之為共享變量。在java中共享變量包括實例變量,靜態變量,數組元素。他們都被存放在堆內存中。
volatile關鍵字
上面扯了一大堆,都沒提到volatile關鍵字的作用,下面開始講解volatile關鍵字是如何保證線程安全問題的。
可見性
什么是可見性?
意思就是說,在多線程環境下,某個共享變量如果被其中一個線程給修改了,其他線程能夠立即知道這個共享變量已經被修改了,當其他線程要讀取這個變量的時候,最終會去內存中讀取,而不是從自己的工作空間中讀取。
例如我們上面說的,當線程1對t進行了加1操作並把數據寫回到主存之后,線程2就會知道它自己工作空間內的t已經被修改了,當它要執行加1操作之后,就會去主存中讀取。這樣,兩邊的數據就能一致了。
假如一個變量被聲明為volatile,那么這個變量就具有了可見性的性質了。這就是volatile關鍵的作用之一了。
volatile保證變量可見性的原理
當一個變量被聲明為volatile時,在編譯成會變指令的時候,會多出下面一行:
0x00bbacde: lock add1 $0x0,(%esp);
這句指令的意思就是在寄存器執行一個加0的空操作。不過這條指令的前面有一個lock(鎖)前綴。
當處理器在處理擁有lock前綴的指令時:
在之前的處理中,lock會導致傳輸數據的總線被鎖定,其他處理器都不能訪問總線,從而保證處理lock指令的處理器能夠獨享操作數據所在的內存區域,而不會被其他處理所干擾。
但由於總線被鎖住,其他處理器都會被堵住,從而影響了多處理器的執行效率。為了解決這個問題,在后來的處理器中,處理器遇到lock指令時不會再鎖住總線,而是會檢查數據所在的內存區域,如果該數據是在處理器的內部緩存中,則會鎖定此緩存區域,處理完后把緩存寫回到主存中,並且會利用緩存一致性協議來保證其他處理器中的緩存數據的一致性。
緩存一致性協議
剛才我在說可見性的時候,說“如果一個共享變量被一個線程修改了之后,當其他線程要讀取這個變量的時候,最終會去內存中讀取,而不是從自己的工作空間中讀取”,實際上是這樣的:
線程中的處理器會一直在總線上嗅探其內部緩存中的內存地址在其他處理器的操作情況,一旦嗅探到某處處理器打算修改其內存地址中的值,而該內存地址剛好也在自己的內部緩存中,那么處理器就會強制讓自己對該緩存地址的無效。所以當該處理器要訪問該數據的時候,由於發現自己緩存的數據無效了,就會去主存中訪問。
有序性
實際上,當我們把代碼寫好之后,虛擬機不一定會按照我們寫的代碼的順序來執行。例如對於下面的兩句代碼:
int a = 1; int b = 2;
對於這兩句代碼,你會發現無論是先執行a = 1還是執行b = 2,都不會對a,b最終的值造成影響。所以虛擬機在編譯的時候,是有可能把他們進行重排序的。
為什么要進行重排序呢?
你想啊,假如執行 int a = 1這句代碼需要100ms的時間,但執行int b = 2這句代碼需要1ms的時間,並且先執行哪句代碼並不會對a,b最終的值造成影響。那當然是先執行int b = 2這句代碼了。
所以,虛擬機在進行代碼編譯優化的時候,對於那些改變順序之后不會對最終變量的值造成影響的代碼,是有可能將他們進行重排序的。
更多代碼編譯優化可以看我寫的另一篇文章:
虛擬機在運行期對代碼的優化策略,那么重排序之后真的不會對代碼造成影響嗎?實際上,對於有些代碼進行重排序之后,雖然對變量的值沒有造成影響,但有可能會出現線程安全問題的。具體請看下面的代碼
public class NoVisibility{ private static boolean ready; private static int number; private static class Reader extends Thread{ public void run(){ while(!ready){ Thread.yield(); } System.out.println(number); } } public static void main(String[] args){ new Reader().start(); number = 42; ready = true; } }
這段代碼最終打印的一定是42嗎?如果沒有重排序的話,打印的確實會是42,但如果number = 42和ready = true被進行了重排序,顛倒了順序,那么就有可能打印出0了,而不是42。(因為number的初始值會是0).
因此,重排序是有可能導致線程安全問題的。
如果一個變量被聲明volatile的話,那么這個變量不會被進行重排序,也就是說,虛擬機會保證這個變量之前的代碼一定會比它先執行,而之后的代碼一定會比它慢執行。
例如把上面中的number聲明為volatile,那么number = 42一定會比ready = true先執行。
不過這里需要注意的是,虛擬機只是保證這個變量之前的代碼一定比它先執行,但並沒有保證這個變量之前的代碼不可以重排序。之后的也一樣。
volatile關鍵字能夠保證代碼的有序性,這個也是volatile關鍵字的作用。
總結一下,一個被volatile聲明的變量主要有以下兩種特性保證保證線程安全。
- 可見性。
- 有序性。
volatile真的能完全保證一個變量的線程安全嗎?
我們通過上面的講解,發現volatile關鍵字還是挺有用的,不但能夠保證變量的可見性,還能保證代碼的有序性。
那么,它真的能夠保證一個變量在多線程環境下都能被正確的使用嗎?
答案是否定的。原因是因為Java里面的運算並非是原子操作。
原子操作
原子操作:即一個操作或者多個操作 要么全部執行並且執行的過程不會被任何因素打斷,要么就都不執行。
也就是說,處理器要嘛把這組操作全部執行完,中間不允許被其他操作所打斷,要嘛這組操作不要執行。
剛才說Java里面的運行並非是原子操作。我舉個例子,例如這句代碼
int a = b + 1;
處理器在處理代碼的時候,需要處理以下三個操作:
- 從內存中讀取b的值。
- 進行a = b + 1這個運算
- 把a的值寫回到內存中
而這三個操作處理器是不一定就會連續執行的,有可能執行了第一個操作之后,處理器就跑去執行別的操作的。
證明volatile無法保證線程安全的例子
由於Java中的運算並非是原子操作,所以導致volatile聲明的變量無法保證線程安全。
對於這句話,我給大家舉個例子。代碼如下:
public class Test{ public static volatile int t = 0; public static void main(String[] args){ Thread[] threads = new Thread[10]; for(int i = 0; i < 10; i++){ //每個線程對t進行1000次加1的操作 threads[i] new Thread(new Runnable(){ @Override public void run(){ for(int j = 0; j < 1000; j++){ t = t + 1; } } }); threads[i].start(); } //等待所有累加線程都結束 while(Thread.activeCount() > 1){ Thread.yield(); } //打印t的值 System.out.println(t); } }
最終的打印結果會是1000 * 10 = 10000嗎?答案是否定的。
問題就出現在t = t + 1這句代碼中。我們來分析一下
例如:
線程1讀取了t的值,假如t = 0。之后線程2讀取了t的值,此時t = 0。
然后線程1執行了加1的操作,此時t = 1。但是這個時候,處理器還沒有把t = 1的值寫回主存中。這個時候處理器跑去執行線程2,注意,剛才線程2已經讀取了t的值,所以這個時候並不會再去讀取t的值了,所以此時t的值還是0,然后線程2執行了對t的加1操作,此時t =1 。
這個時候,就出現了線程安全問題了,兩個線程都對t執行了加1操作,但t的值卻是1。所以說,volatile關鍵字並不一定能夠保證變量的安全性。
什么情況下volatile能夠保證線程安全
剛才雖然說,volatile關鍵字不一定能夠保證線程安全的問題,其實,在大多數情況下volatile還是可以保證變量的線程安全問題的。所以,在滿足以下兩個條件的情況下,volatile就能保證變量的線程安全問題:
- 運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
- 變量不需要與其他狀態變量共同參與不變約束
5. 線程池內的線程如果全部忙,提交一個新的任務,會發⽣什么?隊列全部塞滿了之后,還是忙,再提交會發生什么?
6. Tomcat本身的參數你⼀一般會怎么調整?
1、-Xms :表示java虛擬機堆區內存初始內存分配的大小,通常為操作系統可用內存的1/64大小即可,但仍需按照實際情況進行分配。有可能真的按照這樣的一個規則分配時,設計出的軟件還沒有能夠運行得起來就掛了。
2、-Xmx: 表示java虛擬機堆區內存可被分配的最大上限,通常為操作系統可用內存的1/4大小。但是開發過程中,通常會將 -Xms 與 -Xmx兩個參數的配置相同的值,其目的是為了能夠在java垃圾回收機制清理完堆區后不需要重新分隔計算堆區的大小而浪費資源。
一般來講對於堆區的內存分配只需要對上述兩個參數進行合理配置即可,但是如果想要進行更加精細的分配還可以對堆區內存進一步的細化,那就要用到下面的三個參數了-XX:newSize、-XX:MaxnewSize、-Xmn。當然這源於對堆區的進一步細化分:新生代、中生代、老生代。java中每新new一個對象所占用的內存空間就是新生代的空間,當java垃圾回收機制對堆區進行資源回收后,那些新生代中沒有被回收的資源將被轉移到中生代,中生代的被轉移到老生代。而接下來要講述的三個參數是用來控制新生代內存大小的。
1、-XX:newSize:表示新生代初始內存的大小,應該小於 -Xms的值;
2、-XX:MaxnewSize:表示新生代可被分配的內存的最大上限;當然這個值應該小於 -Xmx的值;
3、-Xmn:至於這個參數則是對 -XX:newSize、-XX:MaxnewSize兩個參數的同時配置,也就是說如果通過-Xmn來配置新生代的內存大小,那么-XX:newSize = -XX:MaxnewSize = -Xmn,雖然會很方便,但需要注意的是這個參數是在JDK1.4版本以后才使用的。
上面所述即為java虛擬機對外提供的可配置堆區的參數,接下來講述java虛擬機對非堆區內存配置的兩個參數:
1、-XX:PermSize:表示非堆區初始內存分配大小,其縮寫為permanent size(持久化內存)
2、-XX:MaxPermSize:表示對非堆區分配的內存的最大上限
7. synchronized關鍵字鎖住的是什么東西?在字節碼中是怎么表示的?在內存中的對象上表現為什么?
a:對象 b:class文件 c:地址
8. wait/notify/notifyAll⽅方法需不需要被包含在synchronized塊中?這是為什么?
要包含,調用wait()就是釋放鎖,釋放鎖的前提是必須要先獲得鎖,先獲得鎖才能釋放鎖
9. ExecutorService你一般是怎么用的?是每個service放一個還是一個項⽬里面放一個?有什么好處?
當使用 ExecutorService 完畢之后,我們應該關閉它,這樣才能保證線程不會繼續保持運行狀態。舉例來說,如果你的程序通過 main() 方法啟動,並且主線程退出了你的程序,如果你還有壹個活動的 ExecutorService 存在於你的程序中,那么程序將會繼續保持運行狀態。存在於 ExecutorService 中的活動線程會阻止Java虛擬機關閉。為了關閉在 ExecutorService 中的線程,你需要調用 shutdown() 方法。ExecutorService 並不會馬上關閉,而是不再接收新的任務,壹但所有的線程結束執行當前任務,ExecutorServie 才會真的關閉。所有在調用 shutdown() 方法之前提交到 ExecutorService 的任務都會執行。如果你希望立即關閉 ExecutorService,你可以調用 shutdownNow() 方法。這個方法會嘗試馬上關閉所有正在執行的任務,並且跳過所有已經提交但是還沒有運行的任務。但是對於正在執行的任務,是否能夠成功關閉它是無法保證的,有可能他們真的被關閉掉了,也有可能它會壹直執行到任務結束。這是壹個最好的嘗試。
Spring
1.你有沒有⽤用過Spring的AOP? 是用來干嘛的? 大概會怎么使用?
2.如果⼀一個接口有2個不同的實現, 那么怎么來Autowire一個指定的實現?
3.Spring的聲明式事務 @Transaction注解一般寫在什么位置? 拋出了異常會自動回滾嗎?有沒有辦法控制不觸發回滾?
4.如果想在某個Bean生成並裝配完畢后執行自己的邏輯,可以什么方式實現?
5.SpringBoot沒有放到web容器里為什么能跑HTTP服務?
6.SpringBoot中如果你想使用自定義的配置文件而不僅僅是application.properties,應該怎么弄?
7.SpringMVC中RequestMapping可以指定GET, POST方法么?怎么指定?
SpringMVC如果希望把輸出的Object(例如
8.XXResult或者XXResponse)這種包裝為JSON輸出, 應該怎么處理?
9.怎樣攔截SpringMVC的異常,然后做自定義的處理,比如打日志或者包裝成JSON
Spring 這里有一個 69 道答案版
MySQL
1.如果有很多數據插入MYSQL 你會選擇什么方式?
2.如果查詢很慢,你會想到的第一個方式是什么?索引是干嘛的?
3.如果建了一個單列索引,查詢的時候查出2列,會用到這個單列索引嗎?
4.如果建了一個包含多個列的索引,查詢的時候只用了第一列,能不能用上這個索引?查三列呢?
5.接上題,如果where條件后面帶有一個 i + 5 < 100 會使用到這個索引嗎?
6.怎么看是否用到了了某個索引?
7.like %aaa%會使用索引嗎? like aaa%呢?
drop、truncate、delete的區別?
8.平時你們是怎么監控數據庫的? 慢SQL是怎么排查的?
9.你們數據庫是否支持emoji表情,如果不支持,如何操作?
10.你們的數據庫單表數據量是多少?一般多大的時候開始出現查詢性能急劇下降?
11查詢死掉了,想要找出執行的查詢進程用什么命令?找出來之后一般你會干嘛?
12.讀寫分離是怎么做的?你認為中間件會怎么來操作?這樣操作跟事務有什么關系?
13.分庫分表有沒有做過?線上的遷移過程是怎么樣的?如何確定數據是正確的?
JVM
1.你知道哪些或者你們線上使用什么GC策略? 它有什么優勢,適用於什么場景?
2.JAVA類加載器包括幾種?它們之間的父子關系是怎么樣的?雙親委派機制是什么意思?有什么好處?
3.如何自定義一個類加載器?你使用過哪些或者你在什么場景下需要一個自定義的類加載器嗎?
堆內存設置的參數是什么?
4.Perm Space中保存什么數據? 會引起OutOfMemory嗎?
5.做gc時,一個對象在內存各個Space中被移動的順序是什么?
6.你有沒有遇到過OutOfMemory問題?你是怎么來處理這個問題的?處理過程中有哪些收獲?
7.1.8之后Perm Space有哪些變動?MetaSpace大小默認是無限的么? 還是你們會通過什么方式來指定大小?
8.Jstack是干什么的? Jstat呢? 如果線上程序周期性地出現卡頓,你懷疑可能是gc導致的,你會怎么來排查這個問題?線程日志一般你會看其中的什么部分?
9.StackOverFlow異常有沒有遇到過?一般你猜測會在什么情況下被觸發?如何指定一個線程的堆棧大小?一般你們寫多少?
Linux命令
1.日志特別大只想看最后100行怎么弄弄? 如果想一直看日志的持續輸出,用什么命令?
2.如果日志一邊輸出,一邊想實時看到有沒有某個關鍵字應該怎么弄?
3.grep如果忽略大小寫應該怎么弄? 正則表達式呢?
4.vim往下一行是什么鍵?往下30行呢? 跳到文件末尾一行是什么? 跳回來是什么? 向后搜索是什么?
5.如果有個文本文件,按空格作為列的分隔符,如果想統計第三列里面的每個單詞的出現次數應該怎么弄?
6.如果把上面的出現次數排個序應該怎么弄? 想按照數字本身的順序而不是字符串的順序排列怎么弄?
7.Linux環境變量是以什么作為分隔符的?環境變量通過什么命令設置?
8.給某個文件權設置限比如設置為64 是用什么命令?這個6是什么意思?
9.Linux下面如果想看某個進程的資源占用情況是怎么看的?系統load大概指的什么意思?你們線上系統load一般多少?如果一個4核機器,你認為多少load是比較正常的?top命令里面按一下1會發生什么?
10.top命令里面,有時候所有進程的CPU使用率加起來超過100%是怎么回事?
11.還有哪些查看系統性能或者供你發現問題的命令?你一般是看哪個參數?
12.想看某個進程打開了哪些網絡連接是什么命令?里面連接的狀態你比較關心哪幾種? -- 偏題
有沒有做過Linux系統參數方面的優化,大概優化過什么?
13.系統參數里面有個叫做backlog的可以用來干什么?
14.查看網絡連接發現好多TIMEWAIT 可能是什么原因?對你的應用會有什么影響?你會選擇什么樣的方式來減少這些TIMEWAIT
15.可否介紹一下TCP三次握手的過程,如果現在有個網絡程序,你用第三方的library來發送數據,你懷疑這個library發送的數據有問題,那么怎么來驗證?tcpdump導出的文件你一般是怎么分析的?
16.KeepAlive是用來干什么的?這樣的好處是什么?
Redis -- 開發
1.緩存穿透可以介紹一下么?你認為應該如何解決這個問題?
2.你是怎么觸發緩存更新的?(比如設置超時時間(被動方式), 比如更新的時候主動update)?如果是被動的方式如何控制多個入口同時觸發某個緩存更新?
3.你們用Redis來做什么?為什么不用其他的KV存儲例例如Memcached,Cassandra等?
4.你們用什么Redis客戶端? Redis高性能的原因大概可以講一些?
5.你熟悉哪些Redis的數據結構? zset是干什么的? 和set有什么區別?
6.Redis的hash, 存儲和獲取的具體命令叫什么名字?
7.LPOP和BLPOP的區別?
8.Redis的有一些包含SCAN關鍵字的命令是干嘛的? SCAN返回的數據量是固定的嗎?
9.Redis中的Lua有沒有使用過? 可以用來做什么? 為什么可以這么用?
10.Redis的Pipeline是用來干什么的? -- 運維
Redis持久化大概有幾種方式? aof和rdb的區別是什么? AOF有什么優缺點嗎?
11.Redis Replication的大致流程是什么? bgsave這個命令的執行過程? -- 偏題
12.如果有很多 KV數據要存儲到Redis, 但是內存不足, 通過什么方式可以縮減內存? 為什么這樣可以縮小內存?
13.Redis中List, HashTable都用到了ZipList, 為什么會選擇它?
Redis 這里有一個 50 道答案版的
監控、穩定性
1.業務日志是通過什么方式來收集的?
2.線上機器如何監控?采用什么開源產品或者自研的產品?它是分鍾級的還是秒級的?
3.如果讓你來想辦法收集一個JAVA后端應用的性能數據,你會在意哪些方面? 你會選擇什么樣的工具、思路來收集?
qi
4.一般你調用第三方的時候會不會監控調用情況?
阿里面題
1.hashcode相等兩個類一定相等嗎?equals呢?相反呢?
2.介紹一下集合框架?
3.hashmap hastable 底層實現什么區別?hashtable和concurrenthashtable呢?
4.hashmap和treemap什么區別?低層數據結構是什么?
5.線程池用過嗎都有什么參數?底層如何實現的?
6.sychnized和Lock什么區別?sychnize 什么情況情況是對象鎖? 什么時候是全局鎖為什么?
7.ThreadLocal 是什么底層如何實現?寫一個例子唄?
8.volitile的工作原理?
9.cas知道嗎如何實現的?
10.請用至少四種寫法寫一個單例模式?
11.請介紹一下JVM內存模型??用過什么垃圾回收器都說說唄
12.線上發送頻繁full gc如何處理? CPU 使用率過高怎么辦?
13.如何定位問題?如何解決說一下解決思路和處理方法
14.知道字節碼嗎?字節碼都有哪些?Integer x =5,int y =5,比較x =y 都經過哪些步驟?
15.講講類加載機制唄都有哪些類加載器,這些類加載器都加載哪些文件?
16.手寫一下類加載Demo
17.知道osgi嗎? 他是如何實現的???
18.請問你做過哪些JVM優化?使用什么方法達到什么效果???
19.classforName("java.lang.String")和String classgetClassLoader() LoadClass("java.lang.String") 什么區別啊?
20.探查Tomcat的運行機制即框架?
21.分析Tomcat線程模型?
22.Tomcat系統參數認識和調優?
23.MySQL底層B+Tree機制?
24.SQL執行計划詳解?
25.索引優化詳解?
26.SQL語句如如如何優化?
27.spring都有哪些機制啊AOP底層如何實現的啊IOC呢??
28.cgLib知道嗎?他和jdk動態代理什么區別?手寫一個jdk動態代理唄?
29.使用mysq1索引都有哪些原則? ?索引什么數據結構? 3+tree 和B tree 什么區別?
30.MySQL有哪些存儲引擎啊?都有啥區別? 要詳細!
31.設計高並發系統數據庫層面該怎么設計??數據庫鎖有哪些類型?如何實現呀?
32.數據庫事務有哪些?
33.如何設計可以動態擴容縮容的分庫分表方案?
34.用過哪些分庫分表中間件,有啥優點和缺點?講一下你了解的分庫分表中間件的底層實現原理?
35.我現在有一個未分庫分表的系統,以后系統需分庫分表,如何設計,讓未分庫分表的系統動態切換到分庫分表的系統上?TCC? 那若出現網絡原因,網絡連不通怎么辦啊?
36.分布式事務知道嗎? 你們怎么解決的?
37.為什么要分庫分表啊?
38.RPC通信原理,分布式通信原理
39.分布式尋址方式都有哪些算法知道一致性hash嗎?手寫一下java實現代碼??你若userId取摸分片,那我要查一段連續時間里的數據怎么辦???
40.如何解決分庫分表主鍵問題有什么實現方案??
41.redis和memcheched 什么區別為什么單線程的redis比多線程的memched效率要高啊?
42.redis有什么數據類型都在哪些場景下使用啊?
43.reids的主從復制是怎么實現的redis的集群模式是如何實現的呢redis的key是如何尋址的啊?
44.使用redis如何設計分布式鎖?使用zk可以嗎?如何實現啊這兩種哪個效率更高啊??
45.知道redis的持久化嗎都有什么缺點優點啊? ?具體底層實現呢?
46.redis過期策略都有哪些LRU 寫一下java版本的代碼吧??
47.說一下dubbo的實現過程注冊中心掛了可以繼續通信嗎??
48.dubbo支持哪些序列化協議?hessian 說一下hessian的數據結構PB知道嗎為啥PB效率是最高的啊??
49.知道netty嗎'netty可以干嘛呀NIO,BIO,AIO 都是什么啊有什么區別啊?
50.dubbo復制均衡策略和高可用策略都有哪些啊動態代理策略呢?
51.為什么要進行系統拆分啊拆分不用dubbo可以嗎'dubbo和thrift什么區別啊?
52.為什么使用消息隊列啊消息隊列有什么優點和缺點啊?
53.如何保證消息隊列的高可用啊如何保證消息不被重復消費啊
54.kafka ,activemq,rabbitmq ,rocketmq都有什么優點,缺點啊???
55.如果讓你寫一個消息隊列,該如何進行架構設計啊?說一下你的思路
56.說一下TCP 'IP四層?
57.的工作流程?? ?http1.0 http1.1http2.0 具體哪些區別啊?
58.TCP三次握手,四層分手的工作流程畫一下流程圖為什么不是四次五次或者二次啊?
59.畫一下https的工作流程?具體如何實現啊?如何防止被抓包啊??
60.源碼中所用到的經典設計思想及常用設計模式
61.系統架構如何選擇合適日志技術(log4j、log4j2、slf4j、jcl…….)
62.springAOP的原理,springAOP和Aspectj的關系,springAOP的源碼問題
63.dubbo框架的底層通信原理
64.RPC通信原理,分布式通信原理
65.如何利用springCloud來架構微服務項目
66.如何正確使用docker技術
67.springMVC的底層原理、如何從源碼來分析其原理
68.mybaits的底層實現原理,如何從源碼來分析mybaits
69.mysql的索引原理,索引是怎么實現的
70.索引的底層算法、如何正確使用、優化索引
71.springboot如何快速構建系統
72.zk原理知道嗎zk都可以干什么Paxos算法知道嗎?說一下原理和實現?
73.如果讓你寫一個消息隊列,該如何進行架構設計啊?說一下你的思路
74.分布式事務知道嗎? 你們怎么解決的?
75.請問你做過哪些JVM優化?使用什么方法達到什么效果