java面試題及答案

何時會觸發full gc

  1. System.gc()方法的調用java

  2. 老年代空間不足程序員

  3. 永生區空間不足(JVM規範中運行時數據區域中的方法區,在HotSpot虛擬機中又被習慣稱爲永生代或者永生區,Permanet Generation中存放的爲一些class的信息、常量、靜態變量等數據)算法

  4. GC時出現promotion failed和concurrent mode failure數據庫

  5. 統計獲得的Minor GC晉升到舊生代平均大小大於老年代剩餘空間bootstrap

  6. 堆中分配很大的對象數組

能夠做爲root的對象:

  1. 類中的靜態變量,當它持有一個指向一個對象的引用時,它就做爲root瀏覽器

  2. 活動着的線程,能夠做爲root緩存

  3. 一個Java方法的參數或者該方法中的局部變量,這兩種對象能夠做爲root安全

  4. JNI方法中的局部變量或者參數,這兩種對象能夠做爲root服務器

例子:下述的Something和Apple均可以做爲root對象。

public AClass{

  public static Something;
  public static final Apple;
   ''''''
}

 

Java方法的參數和方法中的局部變量,能夠做爲root.

public Aclass{

public void doSomething(Object A){
    ObjectB b = new ObjectB; 
    }
 }

 

新生代轉移到老年代的觸發條件

  1. 長期存活的對象

  2. 大對象直接進入老年代

  3. minor gc後,survivor仍然放不下

  4. 動態年齡判斷 ,大於等於某個年齡的對象超過了survivor空間一半 ,大於等於某個年齡的對象直接進入老年代

G1和CMS的區別

  1. G1同時回收老年代和年輕代,而CMS只能回收老年代,須要配合一個年輕代收集器。另外G1的分代更可能是邏輯上的概念,G1將內存分紅多個等大小的region,Eden/ Survivor/Old分別是一部分region的邏輯集合,物理上內存地址並不連續。

    watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=
  2. CMS在old gc的時候會回收整個Old區,對G1來講沒有old gc的概念,而是區分Fully young gc和Mixed gc,前者對應年輕代的垃圾回收,後者混合了年輕代和部分老年代的收集,所以每次收集確定會回收年輕代,老年代根據內存狀況能夠不回收或者回收部分或者所有(這種狀況應該是可能出現)。

 

雙親委派模型中有哪些方法。用戶如何自定義類加載器 。怎麼打破雙親委託機制

  1. 雙親委派模型中用到的方法:

  • findLoadedClass(),

  • loadClass()

  • findBootstrapClassOrNull()

  • findClass()

  • defineClass():把二進制數據轉換成字節碼。

  • resolveClass()

自定義類加載器的方法:繼承 ClassLoader 類,重寫 findClass()方法 。

  1. 繼承ClassLoader覆蓋loadClass方法 原順序

  2. findLoadedClass

  3. 委託parent加載器加載(這裏注意bootstrap加載器的parent爲null)

  4. 自行加載 打破委派機制要作的就是打亂2和3的順序,經過類名篩選本身要加載的類,其餘的委託給parent加載器。

即時編譯器的優化方法

字節碼能夠經過如下兩種方式轉換成合適的語言:

  1. 解釋器

  2. 即時編譯器 即時編譯器把整段字節碼編譯成本地代碼,執行本地代碼比一條一條進行解釋執行的速度快不少,由於本地代碼是保存在緩存裏的

編譯過程的五個階段

  1. 第一階段:詞法分析

  2. 第二階段:語法分析

  3. 第三階段:詞義分析與中間代碼產生

  4. 第四階段:優化

  5. 第五階段:目標代碼生成

java應用系統運行速度慢的解決方法

問題解決思路:

  1. 查看部署應用系統的系統資源使用狀況,CPU,內存,IO這幾個方面去看。找到對就的進程。

  2. 使用jstack,jmap等命令查看是JVM是在在什麼類型的內存空間中作GC(內存回收),和查看GC日誌查看是那段代碼在佔用內存。首先,調節內存的參數設置,若是仍是同樣的問題,就要定位到相應的代碼。

  3. 定位代碼,修改代碼(通常是代碼的邏輯問題,或者代碼獲取的數據量過大。)

 

內存溢出是什麼,什麼緣由致使的

內存溢出是指應用系統中存在沒法回收的內存或使用的內存過多,最終使得程序運行要用到的內存大於虛擬機能提供的最大內存。爲了解決Java中內存溢出問題,咱們首先必須瞭解Java是如何管理內存的。Java的內存管理就是對象的分配和釋放問題。在Java中,內存的分配是由程序完成的,而內存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不須要經過調用GC函數來釋放內存,由於不一樣的JVM實現者可能使用不一樣的算法管理GC,有的是內存使用到達必定程度時,GC纔開始工做,也有定時執行的,有的是中斷式執行GC。但GC只能回收無用而且再也不被其它對象引用的那些對象所佔用的空間。Java的內存垃圾回收機制是從程序的主要運行對象開始檢查引用鏈,當遍歷一遍後發現沒有被引用的孤立對象就做爲垃圾回收。

 

引發內存溢出的緣由有不少種,常見的有如下幾種:

  1. 內存中加載的數據量過於龐大,如一次從數據庫取出過多數據;

  2. 集合類中有對對象的引用,使用完後未清空,使得JVM不能回收;

  3. 代碼中存在死循環或循環產生過多重複的對象實體;

  4. 使用的第三方軟件中的BUG;

  5. 啓動參數內存值設定的太小;

 

內存溢出的解決

內存溢出雖然很棘手,但也有相應的解決辦法,能夠按照從易到難,一步步的解決。

 

第一步,就是修改JVM啓動參數,直接增長內存。這一點看上去彷佛很簡單,但很容易被忽略。JVM默承認以使用的內存爲64M,Tomcat默承認以使用的內存爲128MB,對於稍複雜一點的系統就會不夠用。在某項目中,就由於啓動參數使用的默認值,常常報「OutOfMemory」錯誤。所以,-Xms,-Xmx參數必定不要忘記加。

 

第二步,檢查錯誤日誌,查看「OutOfMemory」錯誤前是否有其它異常或錯誤。在一個項目中,使用兩個數據庫鏈接,其中專用於發送短信的數據庫鏈接使用DBCP鏈接池管理,用戶爲不將短信發出,有意將數據庫鏈接用戶名改錯,使得日誌中有許多數據庫鏈接異常的日誌,一段時間後,就出現「OutOfMemory」錯誤。經分析,這是因爲DBCP鏈接池BUG引發的,數據庫鏈接不上後,沒有將鏈接釋放,最終使得DBCP報「OutOfMemory」錯誤。通過修改正確數據庫鏈接參數後,就沒有再出現內存溢出的錯誤。

 

查看日誌對於分析內存溢出是很是重要的,經過仔細查看日誌,分析內存溢出前作過哪些操做,能夠大體定位有問題的模塊。

 

第三步,找出可能發生內存溢出的位置。重點排查如下幾點:

  1. 檢查代碼中是否有死循環或遞歸調用。

  2. 檢查是否有大循環重複產生新對象實體。

  3. 檢查對數據庫查詢中,是否有一次得到所有數據的查詢。通常來講,若是一次取十萬條記錄到內存,就可能引發內存溢出。這個問題比較隱蔽,在上線前,數據庫中數據較少,不容易出問題,上線後,數據庫中數據多了,一次查詢就有可能引發內存溢出。所以對於數據庫查詢儘可能採用分頁的方式查詢。

  4. 檢查List、MAP等集合對象是否有使用完後,未清除的問題。List、MAP等集合對象會始終存有對對象的引用,使得這些對象不能被GC回收。

 

第四步,使用內存查看工具動態查看內存使用狀況。某個項目上線後,每次系統啓動兩天後,就會出現內存溢出的錯誤。這種狀況通常是代碼中出現了緩慢的內存泄漏,用上面三個步驟解決不了,這就須要使用內存查看工具了。

 

內存查看工具備許多,比較有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它們的基本工做原理大同小異,都是監測Java程序運行時全部對象的申請、釋放等動做,將內存管理的全部信息進行統計、分析、可視化。開發人員能夠根據這些信息判斷程序是否有內存泄漏問題。通常來講,一個正常的系統在其啓動完成後其內存的佔用量是基本穩定的,而不該該是無限制的增加的。持續地觀察系統運行時使用的內存的大小,能夠看到在內存使用監控窗口中是基本規則的鋸齒形的圖線,若是內存的大小持續地增加,則說明系統存在內存泄漏問題。經過間隔一段時間取一次內存快照,而後對內存快照中對象的使用與引用等信息進行比對與分析,能夠找出是哪一個類的對象在泄漏。

 

經過以上四個步驟的分析與處理,基本能處理內存溢出的問題。固然,在這些過程當中也須要至關的經驗與敏感度,須要在實際的開發與調試過程當中不斷積累。

 

整體上來講,產生內存溢出是因爲代碼寫的很差形成的,所以提升代碼的質量是最根本的解決辦法。有的人認爲先把功能實現,有BUG時再在測試階段進行修正,這種想法是錯誤的。正如一件產品的質量是在生產製造的過程當中決定的,而不是質量檢測時決定的,軟件的質量在設計與編碼階段就已經決定了,測試只是對軟件質量的一個驗證,由於測試不可能找出軟件中全部的BUG。

 

JAVA 線程狀態轉換圖示


watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

synchronized 的底層怎麼實現

  1. 同步代碼塊(Synchronization)基於進入和退出管程(Monitor)對象實現。每一個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:

  • 若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。

  • 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.

  • 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權。

  1. 被 synchronized 修飾的同步方法並無經過指令monitorenter和monitorexit來完成(理論上其實也能夠經過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成

講一下CAS

CAS,compare and swap的縮寫,中文翻譯成比較並交換。樂觀鎖用到的機制就是CAS,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試。

 

原理:

  1. CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。

JDK文檔說cas同時具備volatile讀和volatile寫的內存語義。

 

缺點:

  1. ABA問題。由於CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化

  2. 循環時間長開銷大。自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。

  3. 只能保證一個共享變量的原子操做。對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。

線程池

Executor線程池框架是一個根據一組執行策略調用,調度,執行和控制的異步任務的框架。

 

ThreadPoolExecutor執行的策略

  1. 線程數量未達到corePoolSize,則新建一個線程(核心線程)執行任務

  2. 線程數量達到了corePools,則將任務移入隊列等待

  3. 隊列已滿,新建線程(非核心線程)執行任務

  4. 隊列已滿,總線程數又達到了maximumPoolSize,就會由(RejectedExecutionHandler)拋出異常

新建線程 -> 達到核心數 -> 加入隊列 -> 新建線程(非核心) -> 達到最大數 -> 觸發拒絕策略

 

常見四種線程池

  1. CachedThreadPool():可緩存線程池。

  • 線程數無限制

  • 有空閒線程則複用空閒線程,若無空閒線程則新建線程

  • 必定程序減小頻繁建立/銷燬線程,減小系統開銷

  1. FixedThreadPool():定長線程池。

  • 可控制線程最大併發數(同時執行的線程數)

  • 超出的線程會在隊列中等待

  1. ScheduledThreadPool():定時線程池。

  • 支持定時及週期性任務執行。

  1. SingleThreadExecutor():單線程化的線程池。

  • 有且僅有一個工做線程執行任務

  • 全部任務按照指定順序執行,即遵循隊列的入隊出隊規則

 

 

 

 

四種拒絕策略

  1. AbortPolicy:拒絕任務,且還拋出RejectedExecutionException異常,線程池默認策略

  2. CallerRunPolicy:拒絕新任務進入,若是該線程池尚未被關閉,那麼這個新的任務在執行線程中被調用

  3. DiscardOldestPolicy: 若是執行程序還沒有關閉,則位於頭部的任務將會被移除,而後重試執行任務(再次失敗,則重複該過程),這樣將會致使新的任務將會被執行,而先前的任務將會被移除。

  4. DiscardPolicy:沒有添加進去的任務將會被拋棄,也不拋出異常。基本上爲靜默模式。

 

爲何要用線程池

  1. 減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。

  2. 運用線程池能有效的控制線程最大併發數,能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。

  3. 對線程進行一些簡單的管理,好比:延時執行、定時循環執行的策略等,運用線程池都能進行很好的實現

對象鎖和靜態鎖之間的區別
  1. 對象鎖用於對象實例方法,

  2. 類鎖用於類的靜態方法或一個類的class對象。

  3. 類的對象實例能夠有不少,不一樣對象實例的對象鎖互不干擾,而每一個類只有一個類鎖

 

簡述volatile字

兩個特性

  1. 保證了不一樣線程對這個變量進行 讀取 時的可見性,即一個線程修改 了某個變量的值 , 這新值對其餘線程來講是當即可見的 。(volatile 解決了 線程間 共享變量

  2. 禁止進行指令重排序 ,阻止編譯器對代碼的優化

要想併發程序正確地執行,必需要保證原子性、可見性以及有序性,鎖保證了原子性,而volatile保證可見性和有序性

happens-before 原則(先行發生原則):

  1. 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在 後面的操做

  2. 鎖定規則:一個 unLock 操做先行發生於後面對同一個鎖的 lock 操做

  3. volatile 變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做

  4. 傳遞規則:若是操做 A 先行發生於操做 B,而操做 B 又先行發生於操做 C,則能夠 得出操做 A 先行發生於操做 C

  5. 線程啓動規則:Thread 對象的 start()方法先行發生於此線程的每一個一個動做

  6. 線程中斷規則:對線程 interrupt()方法的調用先行發生於被中斷線程的代碼檢測 到中斷事件的發生

  7. 線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過 T hread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行

  8. 對象終結規則:一個對象的初始化完成先行發生於他的 finalize()方法的開始

Lock 和synchronized 的區別

  1. Lock 是一個 接口,而 synchronized 是 Java 中的 關鍵字, synchronized 是 內置的語言實現;

  2. synchronized 在 發生異常時,會 自動釋放線程佔有的鎖,所以 不會導 致死鎖現象發生;而 Lock 在發生異常時,若是沒有主動經過 unLock()去釋放 鎖,則很 可能形成死鎖現象,所以用 使用 Lock 時須要在 finally 塊中釋放鎖;

  3. Lock 可讓 等待鎖的線程響應中斷 (可中斷鎖),而 synchronized 卻不行,使用 synchronized 時,等待的線程會一直等待下去, 不可以響應中 斷 (不可中斷鎖);

  4. 經過 Lock 能夠知道 有沒有成功獲取鎖 (tryLock ( ) 方法 :若是獲取 了鎖 ,回 則返回 true ;回 不然返回 false e, , 也就說這個方法不管如何都會當即返回 。在拿不到鎖時不會一直在那等待。),而 synchronized 卻沒法辦到。

  5. Lock 能夠提升 多個線程進行讀操做的效率( 讀寫鎖)。

  6. Lock 能夠實現 公平鎖,synchronized 不保證公平性。在性能上來講,若是線程競爭資源不激烈時,二者的性能是差很少的,而 當競爭資源很是激烈時(即有大量線程同時競爭),此時 Lock 的性能要遠遠優 於 synchronized。因此說,在具體使用時要根據適當狀況選擇。

 

ThreadLocal(線程變量副本)

Synchronized實現內存共享,ThreadLocal爲每一個線程維護一個本地變量。採用空間換時間,它用於線程間的數據隔離,爲每個使用該變量的線程提供一個副本,每一個線程均可以獨立地改變本身的副本,而不會和其餘線程的副本衝突。ThreadLocal類中維護一個Map,用於存儲每個線程的變量副本,Map中元素的鍵爲線程對象,而值爲對應線程的變量副本。ThreadLocal在Spring中發揮着巨大的做用,在管理Request做用域中的Bean、事務管理、任務調度、AOP等模塊都出現了它的身影。Spring中絕大部分Bean均可以聲明成Singleton做用域,採用ThreadLocal進行封裝,所以有狀態的Bean就可以以singleton的方式在多線程中正常工做了。

經過Callable和Future建立線程

Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很類似,但它能夠返回一個對象或者拋出一個異常。

 

Callable接口使用泛型去定義它的返回類型。Executors類提供了一些有用的方法去在線程池中執行Callable內的任務。因爲Callable任務是並行的,咱們必須等待它返回的結果。java.util.concurrent.Future對象爲咱們解決了這個問題。在線程池提交Callable任務後返回了一個Future對象,使用它咱們能夠知道Callable任務的狀態和獲得Callable返回的執行結果。Future提供了get()方法讓咱們能夠等待Callable結束並獲取它的執行結果。

 

  1. 建立Callable接口的實現類,並實現call()方法,該call()方法將做爲線程執行體,而且有返回值。

  2. 建立Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。

  3. 使用FutureTask對象做爲Thread對象的target建立並啓動新線程。

  4. 調用FutureTask對象的get()方法來得到子線程執行結束後的返回值

 

什麼叫守護線程,用什麼方法實現守護線程(Thread.setDeamon()的含義)

在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程) 用個比較通俗的好比,任何一個守護線程都是整個JVM中全部非守護線程的保姆:只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就;只有當最後一個非守護線程結束時,守護線程隨着JVM一同結束工做。JVM內部的實現是若是運行的程序只剩下守護線程的話,程序將終止運行,直接結束。因此守護線程是做爲輔助線程存在的,主要的做用是提供計數等等輔助的功能。

如何中止一個線程?

終止線程的三種方法:

  1. 使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止。在定義退出標誌exit時,使用了一個Java關鍵字volatile,這個關鍵字的目的是使exit同步,也就是說在同一時刻只能由一個線程來修改exit的值,

  thread.exit = true;  // 終止線程thread 
 
  1. 使用stop方法強行終止線程(這個方法不推薦使用,由於stop和suspend、resume同樣,也可能發生不可預料的結果)。使用stop方法能夠強行終止正在運行或掛起的線程。咱們可使用以下的代碼來終止線程:thread.stop(); 雖然使用上面的代碼能夠終止線程,但使用stop方法是很危險的,就象忽然關閉計算機電源,而不是按正常程序關機同樣,可能會產生不可預料的結果,所以,並不推薦使用stop方法來終止線程。

  2. 使用interrupt方法中斷線程,使用interrupt方法來終端線程可分爲兩種狀況:

  • 線程處於阻塞狀態,如使用了sleep方法。

  • 使用while(!isInterrupted()){……}來判斷線程是否被中斷。在第一種狀況下使用interrupt方法,sleep方法將拋出一個InterruptedException例外,而在第二種狀況下線程將直接退出。

注意:在Thread類中有兩個方法能夠判斷線程是否經過interrupt方法被終止。一個是靜態的方法interrupted(),一個是非靜態的方法isInterrupted(),這兩個方法的區別是interrupted用來判斷當前線是否被中斷,而isInterrupted能夠用來判斷其餘線程是否被中斷。所以,while (!isInterrupted())也能夠換成while (!Thread.interrupted())。

什麼是線程安全?什麼是線程不安全?

  1. 線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。

  2. 線程不安全就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據 在多線程的狀況下,因爲同一進程的多個線程共享同一片存儲空間,在帶來方便的同時,也帶來了訪問衝突這個嚴重的問題。Java語言提供了專門機制以解決這種衝突,有效避免了同一個數據對象被多個線程同時訪問。

 

 

HashSet和TreeSet區別

HashSet

  1. 不能保證元素的排列順序,順序有可能發生變化

  2. 不是同步的

  3. 集合元素能夠是null,但只能放入一個null 當向HashSet結合中存入一個元素時,HashSet會調用該對象的hashCode()方法來獲得該對象的hashCode值,而後根據 hashCode值來決定該對象在HashSet中存儲位置。

TreeSet

  1. TreeSet是SortedSet接口的惟一實現類

  2. TreeSet能夠確保集合元素處於排序狀態。TreeSet支持兩種排序方式,天然排序 和定製排序,其中天然排序爲默認的排序方式。向TreeSet中加入的應該是同一個類的對象

講一下LinkedHashMap

LinkedHashMap的實現就是HashMap+LinkedList的實現方式,以HashMap維護數據結構,以LinkList的方式維護數據插入順序

 

LinkedHashMap保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先獲得的記錄確定是先插入的。在遍歷的時候會比HashMap慢TreeMap可以把它保存的記錄根據鍵排序,默認是按升序排序,也能夠指定排序的比較器

 

利用LinkedHashMap實現LRU算法緩存(

  1. LinkedList首先它是一個Map,Map是基於K-V的,和緩存一致

  2. LinkedList提供了一個boolean值可讓用戶指定是否實現LRU)

 

Java8 中HashMap的優化(引入紅黑樹的數據結構和擴容的優化)

  1. if (binCount >= TREEIFY_THRESHOLD - 1) 當符合這個條件的時候,把鏈表變成treemap紅黑樹,這樣查找效率從o(n)變成了o(log n) ,在JDK1.8的實現中,優化了高位運算的算法,經過hashCode()的高16位異或低16位實現的:

  2. 咱們使用的是2次冪的擴展(指長度擴爲原來2倍),因此,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置

 

這裏的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算。

元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:hashMap 1.8 哈希算法例圖2

 


watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

所以,咱們在擴充HashMap的時候,不須要像JDK1.7的實現那樣從新計算hash,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」

Map遍歷的keySet()和entrySet()性能差別緣由

Set<Entry<StringString>> entrySet = map.entrySet();
Set<Stringset = map.keySet();`
  1. keySet()循環中經過key獲取對應的value的時候又會調用getEntry()進行循環。循環兩次

  2. entrySet()直接使用getEntry()方法獲取結果,循環一次

  3. 因此 keySet()的性能會比entrySet()差點。因此遍歷map的話仍是用entrySet()來遍歷

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }  

 

 

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
}

 

抽象類和接口的對比

 

 

參數 抽象類 接口
默認的方法實現 它能夠有默認的方法實現 接口徹底是抽象的。它根本不存在方法的實現
實現 子類使用extends關鍵字來繼承抽象類。若是子類不是抽象類的話,它須要提供抽象類中全部聲明的方法的實現。 子類使用關鍵字implements來實現接口。它須要提供接口中全部聲明的方法的實現
構造器 抽象類能夠有構造器 接口不能有構造器
與正常Java類的區別 除了你不能實例化抽象類以外,它和普通Java類沒有任何區別 接口是徹底不一樣的類型
訪問修飾符 抽象方法能夠有public、protected和default這些修飾符 接口方法默認修飾符是public。你不可使用其它修飾符。
main方法 抽象方法能夠有main方法而且咱們能夠運行它 接口沒有main方法,所以咱們不能運行它。
多繼承 抽象方法能夠繼承一個類和實現多個接口 接口只能夠繼承一個或多個其它接口
速度 它比接口速度要快 接口是稍微有點慢的,由於它須要時間去尋找在類中實現的方法。
添加新方法 若是你往抽象類中添加新的方法,你能夠給它提供默認的實現。所以你不須要改變你如今的代碼。 若是你往接口中添加方法,那麼你必須改變實現該接口的類。

 

建立一個類的幾種方法?

  1. 使用new關鍵字 → 調用了構造函數

  2. 使用Class類的newInstance方法  → 調用了構造函數

 

Employee emp2 = (Employee)Class.forName("org.programming.mitra.exercises.Employee").newInstance();

 

  1. 使用Constructor類的newInstance方法  → 調用了構造函數

Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
 
  1. 使用clone方法   → 沒有調用構造函數

  2. 使用反序列化 }→ 沒有調用構造函數

ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();

Redirect和forward

  1. 上圖所示的間接轉發請求的過程以下:瀏覽器向Servlet1發出訪問請求;Servlet1調用sendRedirect()方法,將瀏覽器重定向到Servlet2;瀏覽器向servlet2發出請求;最終由Servlet2作出響應。

  2. 上圖所示的直接轉發請求的過程以下:瀏覽器向Servlet1發出訪問請求;Servlet1調用forward()方法,在服務器端將請求轉發給Servlet2;最終由Servlet2作出響應。

 

什麼是泛型,爲何要使用以及類型擦除

  1. 泛型的本質就是「參數化類型」,也就是說所操做的數據類型被指定爲一個參數。建立集合時就指定集合元素的數據類型,該集合只能保存其指定類型的元素, 避免使用強制類型轉換。

  2. Java 編譯器生成的字節碼是不包含泛型信息的,泛型類型信息將在 編譯處理 時 被擦除,這個過程即 類型擦除。類型擦除能夠簡單的理解爲將泛型 java 代碼轉 換爲普通 java 代碼,只不過編譯器更直接點,將泛型 java 代碼直接轉換成普通 java 字節碼。

類型擦除的主要過程以下:

  1. 將全部的泛型參數用其最左邊界(最頂級的父類型)類型替換。

  2. 移除全部的類型參數。

Object跟這些標記符表明的java類型有啥區別呢?

Object是全部類的根類,任何類的對象均可以設置給該Object引用變量,使用的時候可能須要類型強制轉換,可是用使用了泛型T、E等這些標識符後,在實際用以前類型就已經肯定了,不須要再進行類型強制轉換。

Error類和Exception類區別

  1. Error類和Exception類的父類都是throwable類,他們的區別是:Error類通常是指與虛擬機相關的問題,如系統崩潰,虛擬機錯誤,內存空間不足,方法調用棧溢等。對於這類錯誤的致使的應用程序中斷,僅靠程序自己沒法恢復和和預防,遇到這樣的錯誤,建議讓程序終止。Exception類表示程序能夠處理的異常,能夠捕獲且可能恢復。遇到這類異常,應該儘量處理異常,使程序恢復運行, 而不該該隨意終止異常。

  2. Exception類又分爲運行時異常(Runtime Exception)和受檢查的異常(Checked Exception ),運行時異常;ArithmaticException,IllegalArgumentException,編譯能經過,可是一運行就終止了,程序不會處理運行時異常,出現這類異常,程序會終止。而受檢查的異常,要麼用try。。。catch捕獲,要麼用throws字句聲明拋出,交給它的父類處理,不然編譯不會經過。

 

throw和throws區別

throw:(針對對象的作法)拋出一個異常,能夠是系統定義的,也能夠是本身定義的

public void yichang(){
    NumberFormatException e = new NumberFormatException();
    throw e;
}

 

throws:(針對一個方法拋出的異常)拋出一個異常,能夠是系統定義的,也能夠是本身定義的。

public void yichang() throws NumberFormatException{
    int a = Integer.parseInt("10L");
}

 

  1. throws出如今方法函數頭;而throw出如今函數體。

  2. throws表示出現異常的一種可能性,並不必定會發生這些異常;throw則是拋出了異常,執行throw則必定拋出了某種異常。

  3. 二者都是消極處理異常的方式(這裏的消極並非說這種方式很差),只是拋出或者可能拋出異常,可是不會由函數去處理異常,真正的處理異常由函數的上層調用處理。

 

.class 文件是什麼類型文件

class文件是一種8位字節的二進制流文件

java中序列化之子類繼承父類序列化

父類實現了Serializable,子類不須要實現Serializable

 

相關注意事項 1. 序列化時,只對對象的狀態進行保存,而無論對象的方法;2. 當一個父類實現序列化,子類自動實現序列化,不須要顯式實現Serializable接口;c)當一個對象的實例變量引用其餘對象,序列化該對象時也把引用對象進行序列化;3. 並不是全部的對象均可以序列化,至於爲何不能夠,有不少緣由了,好比:1.安全方面的緣由,好比一個對象擁有private,public等field,對於一個要傳輸的對象,好比寫到文件,或者進行rmi傳輸等等,在序列化進行傳輸的過程當中,這個對象的private等域是不受保護的。2. 資源分配方面的緣由,好比socket,thread類,若是能夠序列化,進行傳輸或者保存,也沒法對他們進行從新的資源分配,並且,也是沒有必要這樣實現。

 

2,反過來父類未實現Serializable,子類實現了,序列化子類實例的時候,父類的屬性是直接被跳過不保存,仍是能保存但不能還原?(答案:值不保存)

 

解:父類實現接口後,全部派生類的屬性都會被序列化。子類實現接口的話,父類的屬性值丟失。

 

java中序列化之子類繼承父類序列化

標識符

標識符能夠包括這4種字符:字母、下劃線、$、數字;開頭不能是數字;不能是關鍵字

Integer i=new Integer(127);和Integer i=127;的區別

Integer i = 127的時候,使用Java常量池技術,是爲了方便快捷地建立某些對象,當你須要一個對象時候,就去這個池子裏面找,找不到就在池子裏面建立一個。可是必須注意 若是對象是用new 建立的。那麼無論是什麼對像,它是不會放到池子裏的,而是向堆申請新的空間存儲。Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值在-128到127之間的數時纔可以使用對象池。超過了就要申請空間建立對象了

    int i1=128;
    Integer i2=128;
    Integer i3=new Integer(128);//自動拆箱

    System.out.println(i1==i2);//true
    System.out.println(i1==i3);//true

    Integer i5=127;
    Integer i6=127;
    System.out.println(i5==i6);//true


    Integer i5=127;
    Integer ii5=new Integer(127);
    System.out.println(i5==ii5);//false

    Integer i7=new Integer(127);
    Integer i8=new Integer(127);
    System.out.println(i7==i8);//false

 

手寫單例模式

最好的單例模式是靜態內部類,不要寫雙重檢驗

private static class LazySomethingHolder {
  public static Something something = new Something();
}

public static Something getInstance() {
  return LazySomethingHolder.something;
}

 

爲何線程通訊的方法wait(), notify()和notifyAll()被定義在Object類裏?

Java的每一個對象中都有一個鎖(monitor,也能夠成爲監視器) 而且wait(),notify()等方法用於等待對象的鎖或者通知其餘線程對象的監視器可用。在Java的線程中並無可供任何對象使用的鎖和同步器。這就是爲何這些方法是Object類的一部分,這樣Java的每個類都有用於線程間通訊的基本方法

Java中wait 和sleep 方法比較

  1. 這兩個方法來自不一樣的類分別是Thread和Object

  2. 最主要是sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其餘線程可使用同步控制塊或者方法。

  3. wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep能夠在任何地方使用(使用範圍)

  4. sleep必須捕獲異常,而wait,notify和notifyAll不須要捕獲異常

  5. sleep方法屬於Thread類中方法,表示讓一個線程進入睡眠狀態,等待必定的時間以後,自動醒來進入到可運行狀態,不會立刻進入運行狀態,由於線程調度機制恢復線程的運行也須要時間,一個線程對象調用了sleep方法以後,並不會釋放他所持有的全部對象鎖,因此也就不會影響其餘進程對象的運行。但在sleep的過程當中過程當中有可能被其餘對象調用它的interrupt(),產生InterruptedException異常,若是你的程序不捕獲這個異常,線程就會異常終止,進入TERMINATED狀態,若是你的程序捕獲了這個異常,那麼程序就會繼續執行catch語句塊(可能還有finally語句塊)以及之後的代碼。

  • 注意sleep()方法是一個靜態方法,也就是說他只對當前對象有效,經過t.sleep()讓t對象進入sleep,這樣的作法是錯誤的,它只會是使當前線程被sleep 而不是t線程

  1. wait屬於Object的成員方法,一旦一個對象調用了wait方法,必需要採用notify()和notifyAll()方法喚醒該進程;若是線程擁有某個或某些對象的同步鎖,那麼在調用了wait()後,這個線程就會釋放它持有的全部同步資源,而不限於這個被調用了wait()方法的對象。wait()方法也一樣會在wait的過程當中有可能被其餘對象調用interrupt()方法而產生

 

hashCode和equals方法的關係

在有些狀況下,程序設計者在設計一個類的時候爲須要重寫equals方法,好比String類,可是千萬要注意,在重寫equals方法的同時,必須重寫hashCode方法。也就是說對於兩個對象,若是調用equals方法獲得的結果爲true,則兩個對象的hashcode值一定相等;若是equals方法獲得的結果爲false,則兩個對象的hashcode值不必定不一樣;若是兩個對象的hashcode值不等,則equals方法獲得的結果一定爲false;若是兩個對象的hashcode值相等,則equals方法獲得的結果未知。

Object類中有哪些方法,列舉3個以上(能夠引導)

Object方法:equals()、toString()、finalize()、hashCode()、getClass()、clone()、wait()、notify()、notifyAll()

String s=new String("xyz")究竟建立String Object分爲兩種狀況:

  1. 若是String常理池中,已經建立"xyz",則不會繼續建立,此時只建立了一個對象new String("xyz");

  2. 若是String常理池中,沒有建立"xyz",則會建立兩個對象,一個對象的值是"xyz",一個對象new String("xyz")。

什麼是值傳遞和引用傳遞

值傳遞

public class TempTest {

  private void test1(int a) {
    a = 5;
    System.out.println("test1方法中的a=" + a);
  }

  public static void main(String[] args) {
    TempTest t = new TempTest();
    int a = 3;
    t.test1(11);
    System.out.println("main方法中a=" + a);
  }

}

 

test1方法中的a=5 main方法中a=3 值傳遞:傳遞的是值的拷貝,傳遞後就互不相關了 引用傳遞:傳遞的是變量所對應的內存空間的地址

public class TempTest {
  private void test1(A a) {
    a.age = 20;
    System.out.println("test1方法中a=" + a.age);
  }

  public static void main(String[] args) {
    TempTest t = new TempTest();
    A a = new A();
    a.age = 10;
    t.test1(a);
    System.out.println("main方法中a=" + a.age);
  }
}

class A {
  public int age = 0;
}

 

test1方法中a=20 main方法中a=20 傳遞前和傳遞後都指向同一個引用(同一個內存空間) 若是不互相影響,方法是在test1方法裏面新new一個實例就能夠了

 

講一下netty

netty經過Reactor模型基於多路複用器接收並處理用戶請求,內部實現了兩個線程池,boss線程和work線程池,其中boss線程池的線程負責處理請求的accept事件,當接收到accept事件的請求,把對應的socket封裝到一個NioSocketChannel中,並交給work線程池,其中work線程池負責請求的read和write事件

Nio的原理(同步非阻塞)

服務端和客戶端各自維護一個管理通道的對象,咱們稱之爲 selector,該對 象能檢測一個或多個通道(channel)上的事件。咱們以服務端爲例,若是服務 端的 selector 上註冊了讀事件,某時刻客戶端給服務端送了一些數據,阻塞 I/O 這時會調用 read()方法阻塞地讀取數據,而 NIO 的服務端會在 selector 中添加 一個讀事件。服務端的處理線程會輪詢地訪問 selector,若是訪問 selector 時發 現有感興趣的事件到達,則處理這些事件,若是沒有感興趣的事件到達,則處 理線程會一直阻塞直到感興趣的事件到達爲止。

 


watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

 

緩衝區Buffer、通道Channel、選擇器Selector

緩衝區Buffer

  • 緩衝區其實是一個容器對象,更直接的說,其實就是一個數組,在NIO庫中,全部數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的;在寫入數據時,它也是寫入到緩衝區中的;任什麼時候候訪問 NIO 中的數據,都是將它放到緩衝區中。而在面向流I/O系統中,全部數據都是直接寫入或者直接將數據讀取到Stream對象中。

 

通道Channel

  • 通道是一個對象,經過它能夠讀取和寫入數據,固然了全部數據都經過Buffer對象來處理。咱們永遠不會將字節直接寫入通道中,相反是將數據寫入包含一個或者多個字節的緩衝區。一樣不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。通道與流的不一樣之處在於 通道是雙向 的。而流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類,好比 InputStream 只能進行讀取操做,OutputStream 只能進行寫操做),而通道是雙向的,能夠用於讀、寫或者同時用於讀寫。

 

選擇器(Selector )

  • NIO 有一個主要的類 Selector,這個相似一個觀察者,只要咱們把須要探知 的 socketchannel 告訴 Selector,咱們接着作別的事情, 當有事件發生時,他會 通知咱們,傳回一組 SelectionKey, 咱們讀取這些 Key, 就會得到咱們剛剛註冊 過的 socketchannel, 而後,咱們從這個 Channel 中讀取數據,放心,包準能 夠讀到,接着咱們能夠處理這些數據。

  • Selector 內部原理實際是在作一個 對所註冊的 channel 的輪詢訪問,不斷 地輪詢,一旦輪詢到一個 channel 有所註冊的事情發生,好比數據來了,他就 會站起來報告, 交出一把鑰匙,讓咱們 經過這把鑰匙來讀取這個 channel 的內 容。

 

BIO和NIO的區別

  1. BIO:同步阻塞式IO,服務器實現模式爲一個鏈接一個線程,即客戶端有鏈接請求時服務器端就須要啓動一個線程進行處理,若是這個鏈接不作任何事情會形成沒必要要的線程開銷,固然能夠經過線程池機制改善。

  2. NIO:同步非阻塞式IO,服務器實現模式爲一個請求一個線程,即客戶端發送的鏈接請求都會註冊到多路複用器上,多路複用器輪詢到鏈接有I/O請求時才啓動一個線程進行處理。

NIO的selector做用

Selector(選擇器)是Java NIO中可以檢測一到多個NIO通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個channel,從而管理多個網絡鏈接。

 

爲了實現Selector管理多個SocketChannel,必須將具體的SocketChannel對象註冊到Selector,並聲明須要監聽的事件(這樣Selector才知道須要記錄什麼數據),一共有4種事件:

 

  1. connect:客戶端鏈接服務端事件,對應值爲SelectionKey.OP_CONNECT(8)

  2. accept:服務端接收客戶端鏈接事件,對應值爲SelectionKey.OP_ACCEPT(16)

  3. read:讀事件,對應值爲SelectionKey.OP_READ(1)

  4. write:寫事件,對應值爲SelectionKey.OP_WRITE(4)

 

每次請求到達服務器,都是從connect開始,connect成功後,服務端開始準備accept,準備就緒,開始讀數據,並處理,最後寫回數據返回。

因此,當SocketChannel有對應的事件發生時,Selector均可以觀察到,並進行相應的處理。

相關文章
相關標籤/搜索