前言
依舊如約而至,給大家分享本期的2020春招面試題了,觀看了上篇文章的小夥伴們,也要記得給本篇文章點個贊哦~
那麼話不多說,直接給大家上乾貨、、、
- 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。
- 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,不過又放棄了。
- 常見關鍵字總結: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是屬於對象範疇的東西,而靜態方法是屬於類範疇的東西。
- 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)。
- 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 區別?
-
是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;
-
底層數據結構: Arraylist 底層使用的是Array動態數組;LinkedList 底層使用的是Link鏈表數據結構(JDK1.6之前爲循環鏈表,JDK1.7取消了循環。注意雙向鏈表和雙向循環鏈表的區別)
3.效率不同:
當隨機訪問List(get和set操作)時,ArrayList比LinkedList的效率更高,因爲LinkedList是線性的數據存儲方式,所以需要移動指針從前往後依次查找。
當對數據進行增加和刪除的操作(add和remove操作)時,LinkedList比ArrayList的效率更高,因爲ArrayList是數組,所以在其中進行增刪操作時,會對操作點之後所有數據的下標索引造成影響,需要進行數據的移動。
-
是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應於get(int index)方法)。
-
內存空間佔用: ArrayList的空 間浪費主要體現在在list列表的結尾會預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比ArrayList更多的空間(因爲要存放直接後繼和直接前驅以及數據)。
-
ArrayList 與 Vector 區別呢?爲什麼要用Arraylist取代Vector呢?
Vector類的所有方法都是同步的。可以由兩個線程安全地訪問一個Vector對象、但是一個線程訪問Vector的話代碼要在同步操作上耗費大量的時間。
Arraylist不是同步的,所以在不需要保證線程安全時建議使用Arraylist。
- 說一說 ArrayList 的擴容機制吧
a.ArrayList的構建函數:
**以無參數構造方法創建 ArrayList 時,實際上初始化賦值的是一個空數組。當真正對數組進行添加元素操作時,才真正分配容量。**即向數組中添加第一個元素時,數組容量擴爲10(下面會解釋爲什麼是10)
b.擴容機制分析
這裏以無參構造函數創建的 ArrayList 爲例分析
-
先來看 add 方法
-
再來看看 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方法進行擴容。
- 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 即可