2020春招Java面試題型彙總

前言

依舊如約而至,給大家分享本期的2020春招面試題了,觀看了上篇文章的小夥伴們,也要記得給本篇文章點個贊哦~

那麼話不多說,直接給大家上乾貨、、、

  1. Java中的異常處理

​ 在Java中,所有的異常都有一個共同的祖先java.lang包中的Throwable類。

Throwable包含兩個子類:

Exception(異常):

​ 是程序本身可以處理的異常。Exception 類有一個重要的子類 RuntimeException。RuntimeException 異常由Java虛擬機拋出。NullPointerException(要訪問的變量沒有引用任何對象時,拋出該異常)、ArithmeticException(算術運算異常,一個整數除以0時,拋出該異常)和 ArrayIndexOutOfBoundsException (下標越界異常)。

Error(錯誤):

​ 是程序無法處理的錯誤,表示運行應用程序中較嚴重問題。大多數錯誤與代碼編寫者執行的操作無關,而表示代碼運行時 JVM(Java 虛擬機)出現的問題。例如,Java虛擬機運行錯誤(Virtual MachineError),當 JVM 不再有繼續執行操作所需的內存資源時,將出現 OutOfMemoryError。這些異常發生時,Java虛擬機(JVM)一般會選擇線程終止。

這些錯誤表示故障發生於虛擬機自身、或者發生在虛擬機試圖執行應用時,如Java虛擬機運行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因爲它們在應用程序的控制和處理能力之 外,而且絕大多數是程序運行時不允許出現的狀況。對於設計合理的應用程序來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在 Java中,錯誤通過Error的子類描述。

注意:異常和錯誤的區別:異常能被程序本身處理,錯誤是無法處理。

Throwable類常用方法

public string getMessage():返回異常發生時的簡要描述

public string toString():返回異常發生時的詳細信息

public string getLocalizedMessage():返回異常對象的本地化信息。使用Throwable的子類覆蓋這個方法,可以生成本地化信息。如果子類沒有覆蓋該方法,則該方法返回的信息與getMessage()返回的結果相同

public void printStackTrace():在控制檯上打印Throwable對象封裝的異常信息

異常處理總結

try 塊: 用於捕獲異常。其後可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。

catch 塊: 用於處理try捕獲到的異常。

finally 塊: 無論是否捕獲或處理異常,finally塊裏的語句都會被執行。當在try塊或catch塊中遇到return 語句時,finally語句塊將在方法返回之前被執行。

eg:

如果調用 f(6),返回值將是0,因爲finally語句的返回值覆蓋了try語句塊的返回值。

在以下4種特殊情況下,finally塊不會被執行:

在finally語句塊第一行發生了異常。 因爲在其他行,finally塊還是會得到執行

在前面的代碼中用了System.exit(int)已退出程序。 exit是帶參函數 ;若該語句在異常語句之後,finally會執行

程序所在的線程死亡。

關閉CPU。

  1. Java中的IO流

按照流的流向分,可以分爲輸入流和輸出流;

按照操作單元劃分,可以劃分爲字節流和字符流;

按照流的角色劃分爲節點流和處理流。

Java Io流共涉及40多個類,這些類看上去很雜亂,但實際上很有規則,而且彼此之間存在非常緊密的聯繫, Java I0流的40多個類都是從如下4個抽象類基類中派生出來的。

InputStream/Reader: 所有的輸入流的基類,前者是字節輸入流,後者是字符輸入流。

OutputStream/Writer: 所有輸出流的基類,前者是字節輸出流,後者是字符輸出流。

按操作方式分類結構圖:

按操作方式分類結構圖:

問題:

​在文件讀寫和網絡發送接收中,信息的最小存儲單位都是字節,那爲什麼I/O流操作要分爲字節流操作和字符流操作呢?

答:

​字符流是Java虛擬機將字節轉換得到的,問題就出在這個過程算是非常耗時,並且,如果我們不知道編碼類型就轉爲字節流容易出現亂碼現象。因此,I/O流就直接提供了一個操作字符的接口,方便我們平時對字符進行操作。如果音頻文件、圖片等媒體文件用字節流比較好,如果涉及到字符的話使用字符流比較好。

BIO,NIO,AIO 有什麼區別?

BIO (Blocking I/O): 同步阻塞I/O模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。在活動連接數不是特別高(小於單機1000)的情況下,這種模型是比較不錯的,可以讓每一個連接專注於自己的 I/O 並且編程模型簡單,也不用過多考慮系統的過載、限流等問題。線程池本身就是一個天然的漏斗,可以緩衝一些系統處理不了的連接或請求。但是,當面對十萬甚至百萬級連接的時候,傳統的 BIO 模型是無能爲力的。因此,我們需要一種更高效的 I/O 處理模型來應對更高的併發量。

NIO (New I/O): NIO是一種同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,對應 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解爲Non-blocking,不單純是New。它支持面向緩衝的,基於通道的I/O操作方法。 NIO提供了與傳統BIO模型中的 Socket 和 ServerSocket 相對應的 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。對於低負載、低併發的應用程序,可以使用同步阻塞I/O來提升開發速率和更好的維護性;對於高負載、高併發的(網絡)應用,應使用 NIO 的非阻塞模式來開發

AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進版 NIO 2,它是異步非阻塞的IO模型。異步 IO 是基於事件和回調機制實現的,也就是應用操作之後會直接返回,不會堵塞在那裏,當後臺處理完成,操作系統會通知相應的線程進行後續的操作。AIO 是異步IO的縮寫,雖然 NIO 在網絡操作中,提供了非阻塞的方法,但是 NIO 的 IO 行爲還是同步的。對於 NIO 來說,我們的業務線程是在 IO 操作準備好時,得到通知,接着就由這個線程自行進行 IO 操作,IO操作本身是同步的。查閱網上相關資料,我發現就目前來說 AIO 的應用還不是很廣泛,Netty 之前也嘗試使用過 AIO,不過又放棄了。

  1. 常見關鍵字總結:static,final,this,super

final 關鍵字

​final關鍵字主要用在三個地方:變量、方法、類。

1、對於一個final變量,如果是基本數據類型的變量,則其數值一旦在初始化之後便不能更改;如果是引用類型的變量,則在對其初始化之後便不能再讓其指向另一個對象。

2、當用final修飾一個類時,表明這個類不能被繼承。final類中的所有成員方法都會被隱式地指定爲final方法。

3、使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率。在早期的Java實現版本中,會將final方法轉爲內嵌調用。但是如果方法過於龐大,可能看不到內嵌調用帶來的任何性能提升(現在的Java版本已經不需要使用final方法進行這些優化了)。類中所有的private方法都隱式地指定爲final。

static 關鍵字

​static 關鍵字主要有以下四種使用場景:

修飾成員變量和成員方法: 被 static 修飾的成員屬於類,不屬於單個這個類的某個對象,被類中所有對象共享,可以並且建議通過類名調用。被static 聲明的成員變量屬於靜態成員變量,靜態變量 存放在 Java 內存區域的方法區。調用格式:類名.靜態變量名 類名.靜態方法名()

靜態代碼塊: 靜態代碼塊定義在類中方法外, 靜態代碼塊在非靜態代碼塊之前執行(靜態代碼塊—>非靜態代碼塊—>構造方法)。 該類不管創建多少對象,靜態代碼塊只執行一次.

靜態內部類(static修飾類的話只能修飾內部類): 靜態內部類與非靜態內部類之間存在一個最大的區別: 非靜態內部類在編譯完成之後會隱含地保存着一個引用,該引用是指向創建它的外圍類,但是靜態內部類卻沒有。沒有這個引用就意味着:1. 它的創建是不需要依賴外圍類的創建。2. 它不能使用任何外圍類的非static成員變量和方法。

靜態導包(用來導入類中的靜態資源,1.5之後的新特性): 格式爲:import static 這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,並且不需要使用類名調用類中靜態成員,可以直接使用類中靜態成員變量和成員方法。

this 關鍵字

​this關鍵字用於引用類的當前實例。

在上面的示例中,this關鍵字用於兩個地方:

this.employees.length:訪問類Manager的當前實例的變量。

this.report():調用類Manager的當前實例的方法。

此關鍵字是可選的,這意味着如果上面的示例在不使用此關鍵字的情況下表現相同。 但是,使用此關鍵字可能會使代碼更易讀或易懂。

super 關鍵字

​super關鍵字用於從子類訪問父類的變量和方法。

在上面的例子中,Sub 類訪問父類成員變量 number 並調用其其父類 Super 的 showNumber() 方法。

使用 this 和 super 要注意的問題:

在構造器中使用 super() 調用父類中的其他構造方法時,該語句必須處於構造器的首行,否則編譯器會報錯。另外,this 調用本類中的其他構造方法時,也要放在首行。

this、super不能用在static方法中。

簡單解釋一下:

被 static 修飾的成員屬於類,不屬於單個這個類的某個對象,被類中所有對象共享。而 this 代表對本類對象的引用,指向本類對象;而 super 代表對父類對象的引用,指向父類對象;所以, this和super是屬於對象範疇的東西,而靜態方法是屬於類範疇的東西。

  1. Servlet總結

​ 在Java Web程序中,Servlet主要負責接收用戶請求 HttpServletRequest,在doGet(),doPost()中做相應的處理,並將迴應HttpServletResponse反饋給用戶。Servlet 可以設置初始化參數,供Servlet內部使用。一個Servlet類只會有一個實例,在它初始化時調用init()方法,銷燬時調用destroy()方法**。**Servlet需要在web.xml中配置(MyEclipse中創建Servlet會自動配置),一個Servlet可以設置多個URL訪問。Servlet不是線程安全,因此要謹慎使用類變量。

​ Servlet接口定義了5個方法,其中前三個方法與Servlet生命週期相關:

生命週期:

​ Web容器加載Servlet並將其實例化後,Servlet生命週期開始,容器運行其init()方法進行Servlet的初始化; 請求到達時調用Servlet的service()方法,service()方法會根據需要調用與請求對應的doGet或doPost等方法;當服務器關閉或項目被卸載時服務器會將Servlet實例銷燬,此時會調用Servlet的destroy()方法。

​ init方法和destroy方法只會執行一次,service方法客戶端每次請求Servlet都會執行。Servlet中有時會用到一些需要初始化與銷燬的資源,因此可以把初始化資源的代碼放入init方法中,銷燬資源的代碼放入destroy方法中,這樣就不需要每次處理客戶端的請求都要初始化與銷燬資源。

get和post請求的區別:

​ 可以把 get 和 post 當作兩個不同的行爲,兩者並沒有什麼本質區別,底層都是 TCP 連接。 get請求用來從服務器上獲得資源,而post是用來向服務器提交數據。比如你要獲取人員列表可以用 get 請求,你需要創建一個人員可以用 post 。這也是 Restful API 最基本的一個要求。

轉發(Forward)和重定向(Redirect)的區別:

​ 轉發是服務器行爲,重定向是客戶端行爲。

​轉發(Forward) 通過RequestDispatcher對象的forward(HttpServletRequest request,HttpServletResponse response)方法實現的。RequestDispatcher可以通過HttpServletRequest 的getRequestDispatcher()方法獲得。例如下面的代碼就是跳轉到login_success.jsp頁面。

重定向(Redirect) 是利用服務器返回的狀態碼來實現的。客戶端瀏覽器請求服務器的時候,服務器會返回一個狀態碼。服務器通過 HttpServletResponse 的 setStatus(int status) 方法設置狀態碼。如果服務器返回301或者302,則瀏覽器會到新的網址重新請求該資源。

1、從地址欄顯示來說

​ forward是服務器請求資源,服務器直接訪問目標地址的URL,把那個URL的響應內容讀取過來,然後把這些內容再發給瀏覽器.瀏覽器根本不知道服務器發送的內容從哪裏來的,所以它的地址欄還是原來的地址. redirect是服務端根據邏輯,發送一個狀態碼,告訴瀏覽器重新去請求那個地址.所以地址欄顯示的是新的URL.

2、從數據共享來說

​ forward:轉發頁面和轉發到的頁面可以共享request裏面的數據. redirect:不能共享數據.

3、從運用地方來說

​ forward:一般用於用戶登陸的時候,根據角色轉發到相應的模塊. redirect:一般用於用戶註銷登陸時返回主頁面和跳轉到其它的網站等

4、從效率來說

​ forward:高. redirect:低.

自動刷新(Refresh)

​ 自動刷新不僅可以實現一段時間之後自動跳轉到另一個頁面,還可以實現一段時間之後自動刷新本頁面。Servlet中通過HttpServletResponse對象設置Header屬性實現自動刷新例如:

JSP和Servlet是什麼關係

​ Servlet是一個特殊的Java程序,它運行於服務器的JVM中,能夠依靠服務器的支持向瀏覽器提供顯示內容。JSP本質上是Servlet的一種簡易形式,JSP會被服務器處理成一個類似於Servlet的Java程序,可以簡化頁面內容的生成。Servlet和JSP最主要的不同點在於,Servlet的應用邏輯是在Java文件中,並且完全從表示層中的HTML分離開來。而JSP的情況是Java和HTML可以組合成一個擴展名爲.jsp的文件。有人說,Servlet就是在Java中寫HTML,而JSP就是在HTML中寫Java代碼,當然這個說法是很片面且不夠準確的。JSP側重於視圖,Servlet更側重於控制邏輯,在MVC架構模式中,JSP適合充當視圖(view)而Servlet適合充當控制器(controller)。

  1. JSP總結

​ JSP是一種類Servlet,但是與HttpServlet的工作方式不太一樣。HttpServlet是先由源代碼編譯爲class文件後部署到服務器下,爲先編譯後部署。而JSP則是先部署後編譯。JSP會在客戶端第一次請求JSP文件時被編譯爲HttpJspPage類(接口Servlet的一個子類)。該類會被服務器臨時存放在服務器工作目錄裏面。

JSP內置對象:

​JSP有9個內置對象:

request:封裝客戶端的請求,其中包含來自GET或POST請求的參數;

response:封裝服務器對客戶端的響應;

pageContext:通過該對象可以獲取其他對象;

session:封裝用戶會話的對象;

application:封裝服務器運行環境的對象;

out:輸出服務器響應的輸出流對象;

config:Web應用的配置對象;

page:JSP頁面本身(相當於Java程序中的this);

exception:封裝頁面拋出異常的對象。

Request對象的主要方法有哪些

setAttribute(String name,Object):設置名字爲name的request 的參數值

getAttribute(String name):返回由name指定的屬性值

getAttributeNames():返回request 對象所有屬性的名字集合,結果是一個枚舉的實例

getCookies():返回客戶端的所有 Cookie 對象,結果是一個Cookie 數組

getCharacterEncoding() :返回請求中的字符編碼方式 = getContentLength() :返回請求的 Body的長度

getHeader(String name) :獲得HTTP協議定義的文件頭信息

getHeaders(String name) :返回指定名字的request Header 的所有值,結果是一個枚舉的實例

getHeaderNames() :返回所以request Header 的名字,結果是一個枚舉的實例

getInputStream() :返回請求的輸入流,用於獲得請求中的數據

getMethod() :獲得客戶端向服務器端傳送數據的方法

getParameter(String name) :獲得客戶端傳送給服務器端的有 name指定的參數值

getParameterNames() :獲得客戶端傳送給服務器端的所有參數的名字,結果是一個枚舉的實例

getParameterValues(String name):獲得有name指定的參數的所有值

getProtocol():獲取客戶端向服務器端傳送數據所依據的協議名稱

getQueryString() :獲得查詢字符串

getRequestURI() :獲取發出請求字符串的客戶端地址

getRemoteAddr():獲取客戶端的 IP 地址

getRemoteHost() :獲取客戶端的名字

getSession([Boolean create]) :返回和請求相關 Session

getServerName() :獲取服務器的名字

getServletPath():獲取客戶端所請求的腳本文件的路徑

getServerPort():獲取服務器的端口號

removeAttribute(String name):刪除請求中的一個屬性

JSP中的四種作用域

JSP中的四種作用域包括page、request、session和application,具體來說:

page代表與一個頁面相關的對象和屬性。

request代表與Web客戶機發出的一個請求相關的對象和屬性。一個請求可能跨越多個頁面,涉及多個Web組件;需要在頁面顯示的臨時數據可以置於此作用域。

session代表與某個用戶與服務器建立的一次會話相關的對象和屬性。跟某個用戶相關的數據應該放在用戶自己的session中。

application代表與整個Web應用程序相關的對象和屬性,它實質上是跨越整個Web應用程序,包括多個頁面、請求和會話的一個全局作用域。

6.說說List,Set,Map三者的區別?

List(對付順序的好幫手): List接口存儲一組不唯一(可以有多個元素引用相同的對象),有序的對象

Set(注重獨一無二的性質): 不允許重複的集合。不會有多個元素引用相同的對象。

Map(用Key來搜索的專家): 使用鍵值對存儲。Map會維護與Key有關聯的值。兩個Key可以引用相同的對象,但Key不能重複,典型的Key是String類型,但也可以是任何對象。

7.Arraylist 與 LinkedList 區別?

  1. 是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;

  2. 底層數據結構: Arraylist 底層使用的是Array動態數組;LinkedList 底層使用的是Link鏈表數據結構(JDK1.6之前爲循環鏈表,JDK1.7取消了循環。注意雙向鏈表和雙向循環鏈表的區別)

3.效率不同:

當隨機訪問List(get和set操作)時,ArrayList比LinkedList的效率更高,因爲LinkedList是線性的數據存儲方式,所以需要移動指針從前往後依次查找。

當對數據進行增加和刪除的操作(add和remove操作)時,LinkedList比ArrayList的效率更高,因爲ArrayList是數組,所以在其中進行增刪操作時,會對操作點之後所有數據的下標索引造成影響,需要進行數據的移動。

  1. 是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應於get(int index)方法)。

  2. 內存空間佔用: ArrayList的空 間浪費主要體現在在list列表的結尾會預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比ArrayList更多的空間(因爲要存放直接後繼和直接前驅以及數據)。

  3. ArrayList 與 Vector 區別呢?爲什麼要用Arraylist取代Vector呢?

​ Vector類的所有方法都是同步的。可以由兩個線程安全地訪問一個Vector對象、但是一個線程訪問Vector的話代碼要在同步操作上耗費大量的時間。

​ Arraylist不是同步的,所以在不需要保證線程安全時建議使用Arraylist。

  1. 說一說 ArrayList 的擴容機制吧

a.ArrayList的構建函數:

**以無參數構造方法創建 ArrayList 時,實際上初始化賦值的是一個空數組。當真正對數組進行添加元素操作時,才真正分配容量。**即向數組中添加第一個元素時,數組容量擴爲10(下面會解釋爲什麼是10)

b.擴容機制分析

​ 這裏以無參構造函數創建的 ArrayList 爲例分析

  1. 先來看 add 方法

  2. 再來看看 ensureCapacityInternal() 方法

可以看到 add 方法 首先調用了ensureCapacityInternal(size + 1)

當 要 add 進第1個元素時,minCapacity爲1,在Math.max()方法比較後,minCapacity 爲10。

3. ensureExplicitCapacity() 方法

當我們要 add 進第1個元素到 ArrayList 時,elementData.length 爲0 (因爲還是一個空的 list),因爲執行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此時爲10。此時,minCapacity - elementData.length > 0成立,所以會進入 grow(minCapacity) 方法。

當add第2個元素時,minCapacity 爲2,此時e lementData.length(容量)在添加第一個元素後擴容成 10 了。此時,minCapacity - elementData.length > 0不成立,所以不會進入 (執行)grow(minCapacity) 方法。

添加第3、4···到第10個元素時,依然不會執行grow方法,數組容量都爲10。

直到添加第11個元素,minCapacity(爲11)比elementData.length(爲10)要大。進入grow方法進行擴容。

  1. grow() 方法

int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次擴容之後容量都會變爲原來的 1.5 倍!(JDK1.6版本以後) JDk1.6版本時,擴容之後容量爲 1.5 倍+1!

當add第1個元素時,oldCapacity 爲0,經比較後第一個if判斷成立,newCapacity = minCapacity(爲10)。但是第二個if判斷不會成立,即newCapacity 不比 MAX_ARRAY_SIZE大,則不會進入 hugeCapacity 方法。數組容量爲10,add方法中 return true,size增爲1。

當add第11個元素進入grow方法時,newCapacity爲15,比minCapacity(爲11)大,第一個if判斷不成立。新容量沒有大於數組最大size,不會進入hugeCapacity方法。數組容量擴爲15,add方法中return true,size增爲11。

以此類推······

​ 5. hugeCapacity() 方法。

從上面 grow() 方法源碼我們知道: 如果新容量大於 MAX_ARRAY_SIZE,進入(執行) hugeCapacity() 方法來比較 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大於最大容量,則新容量則爲Integer.MAX_VALUE,否則,新容量大小則爲 MAX_ARRAY_SIZE 即爲 Integer.MAX_VALUE - 8。

10.HashMap 和 Hashtable 的區別

線程是否安全: HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 內部的方法基本都經過synchronized 修飾。(如果你要保證線程安全的話就使用 ConcurrentHashMap 吧!);

效率: 因爲線程安全的問題,HashMap 要比 HashTable 效率高一點。另外,HashTable 基本被淘汰,不要在代碼中使用它;

對Null key 和Null value的支持: HashMap 中,null 可以作爲鍵,這樣的鍵只有一個,可以有一個或多個鍵所對應的值爲 null。。但是在 HashTable 中 put 進的鍵值只要有一個 null,直接拋出 NullPointerException。

初始容量大小和每次擴充容量大小的不同 : ①創建時如果不指定容量初始值,Hashtable 默認的初始大小爲11,之後每次擴充,容量變爲原來的2n+1。HashMap 默認的初始化大小爲16。之後每次擴充,容量變爲原來的2倍。②創建時如果給定了容量初始值,那麼 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充爲2的冪次方大小(HashMap 中的tableSizeFor()方法保證,下面給出了源代碼)。也就是說 HashMap 總是使用2的冪作爲哈希表的大小,後面會介紹到爲什麼是2的冪次方。

底層數據結構: JDK1.8 以後的 HashMap 在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減少搜索時間。Hashtable 沒有這樣的機制。

11.HashMap 和 HashSet區別

​ 如果你看過 HashSet 源碼的話就應該知道:HashSet 底層就是基於 HashMap 實現的。(HashSet 的源碼非常非常少,因爲除了 clone()、writeObject()、readObject()是 HashSet 自己不得不實現之外,其他方法都是直接調用 HashMap 中的方法。

HashSet如何檢查重複

​當你把對象加入HashSet時,HashSet會先計算對象的hashcode值來判斷對象加入的位置,同時也會與其他加入的對象的hashcode值作比較,如果沒有相符的hashcode,HashSet會假設對象沒有重複出現。但是如果發現有相同hashcode值的對象,這時會調用equals()方法來檢查hashcode相等的對象是否真的相同。如果兩者相同,HashSet就不會讓加入操作成功。(摘自我的Java啓蒙書《Head fist java》第二版)

12.ConcurrentHashMap 和 Hashtable 的區別

​ ConcurrentHashMap 和 Hashtable 的區別主要體現在實現線程安全的方式上不同。

底層數據結構: JDK1.7的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是採用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的;

實現線程安全的方式(重要): ① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不同數據段的數據,就不會存在鎖競爭,提高併發訪問率。 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6以後 對 synchronized鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低。

HashMap 多線程操作導致死循環問題

主要原因在於 併發下的Rehash 會造成元素之間會形成一個循環鏈表。不過,jdk 1.8 後解決了這個問題,但是還是不建議在多線程下使用 HashMap,因爲多線程下使用 HashMap 還是會存在其他問題比如數據丟失。併發環境下推薦使用 ConcurrentHashMap 。

最後,還是老樣子,本文全部內容小編已整理成PDF文檔形式,如需免費獲取請點擊下方鏈接:https://shimo.im/docs/pVhDCpYqdRTqWRqq/read 即可

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章