Java知識分享網 - 輕松學習從此開始!????

Java知識分享網

Java1234官方群24:java1234官方群24
Java1234官方群24:791563025
     

Mycat實現mysql高可用集群視頻教程免費領取

畢設代做,包查重聯系人QQ:1982956321畢設大神

領取國內優秀就業,加薪,跳槽項目課程源碼-vue2+jwt+springboot+mybaits前后端分離通訊錄系統課程

SpringBoot打造企業級進銷存

Java1234 VIP課程

領取微信掃碼登錄Java實現視頻教程

Java1234至尊VIP(特價活動)

2017-2020年字節跳動Android面條題匯總附答案 PDF 下


分享到:
時間:2020-03-29 20:56來源:http://www.luygg.com 作者:小鋒  侵權舉報
2017-2020年字節跳動Android面條題匯總附答案 PDF 下載
失效鏈接處理
2017-2020年字節跳動Android面條題匯總附答案 PDF 下載

下載地址:

提取碼:jnqq

相關截圖:


主要內容:

2017-2020 字節跳動 Android 面試真題
解析
目錄
2017-2020 字節跳動 Android 面試真題解析................................................................................... 1
目錄..................................................................................................................................................... 1
第一章計算機基礎面試題.........................................................................................................1
第一節、網絡面試題.........................................................................................................1
第二節、操作系統面試題 (???)........................................................................ 21
第三節、數據庫面試題 (?).................................................................................... 23
第二章 數據結構和算法面試題.............................................................................................25
數據結構與算法...............................................................................................................25
第三章 Java 面試題..................................................................................................................33
第一節 Java 基礎面試題..................................................................................................33
第二節 Java 并發面試題.................................................................................................81
第三節 Java 虛擬機面試題 (???)......................................................................121
第四章 Android 面試題.........................................................................................................140
第一節 Android 基礎面試題 (???).................................................................. 140
第二節 Android 高級面試題 (???)................................................................... 208
第五章 其他擴展面試題.......................................................................................................346
一、Kotlin (??).................................................................................................... 346
二、大前端 (??)...................................................................................................346
三、腳本語言 (??)...............................................................................................349
第六章非技術面試題.............................................................................................................350
一、高頻題集 (???)...........................................................................................350
二、次高頻題集 (??)...........................................................................................352
第一章計算機基礎面試題
第一節、網絡面試題
一、HTTP/HTTPS (???)1、HTTP 與 HTTPS 有什么區別?
HTTPS 是一種通過計算機網絡進行安全通信的傳輸協議。HTTPS 經由 HTTP 進行通信,但利
用 SSL/TLS 來加密數據包。HTTPS 開發的主要目的,是提供對網站服務器的身份 認證,保
護交換數據的隱私與完整性。
HTPPS 和 HTTP 的概念:
HTTPS(全稱:Hypertext Transfer Protocol over Secure Socket Layer),是以安全為目標
的 HTTP 通道,簡單講是 HTTP 的安全版。即 HTTP 下加入 SSL 層,HTTPS 的安全基礎是 SSL,
因此加密的詳細內容就需要 SSL。它是一個 URI scheme(抽象標識符體系),句法類同 http:
體系。用于安全的 HTTP 數據傳輸。https:URL 表明它使用了 HTTP,但 HTTPS 存在不同于
HTTP 的默認端口及一個加密/身份驗證層(在 HTTP 與 TCP 之間)。這個系統的最初研發由
網景公司進行,提供了身份驗證與加密通訊方法,現在它被廣泛用于萬維網上安全敏感的通
訊,例如交易支付方面。
超文本傳輸協議 (HTTP-Hypertext transfer protocol) 是一種詳細規定了瀏覽器和萬維網服
務器之間互相通信的規則,通過因特網傳送萬維網文檔的數據傳送協議。
https 協議需要到 ca 申請證書,一般免費證書很少,需要交費。http 是超文本傳輸協議,
信息是明文傳輸,https 則是具有安全性的 ssl 加密傳輸協議 http 和 https 使用的是完全不
同的連接方式用的端口也不一樣,前者是 80,后者是 443。http 的連接很簡單,是無狀態的
HTTPS 協議是由 SSL+HTTP 協議構建的可進行加密傳輸、身份認證的網絡協議 要比 http 協
議安全 HTTPS 解決的問題:1 . 信任主機的問題. 采用 https 的 server 必須從 CA 申請一個
用于證明服務器用途類型的證書. 改證書只有用于對應的 server 的時候,客戶度才信任次主
機 2 . 防止通訊過程中的數據的泄密和被竄改
如下圖所示,可以很明顯的看出兩個的區別:
注:TLS 是 SSL 的升級替代版,具體發展歷史可以參考傳輸層安全性協議。HTTP 與 HTTPS 在寫法上的區別也是前綴的不同,客戶端處理的方式也不同,具體說來:
如果 URL 的協議是 HTTP,則客戶端會打開一條到服務端端口 80(默認)的連接,并向其
發送老的 HTTP 請求。 如果 URL 的協議是 HTTPS,則客戶端會打開一條到服務端端口 443
(默認)的連接,然后與服務器握手,以二進制格式與服務器交換一些 SSL 的安全參數,附
上加密的 HTTP 請求。 所以你可以看到,HTTPS 比 HTTP 多了一層與 SSL 的連接,這也就
是客戶端與服務端 SSL 握手的過程,整個過程主要完成以下工作:
交換協議版本號 選擇一個兩端都了解的密碼 對兩端的身份進行認證 生成臨時的會話密
鑰,以便加密信道。 SSL 握手是一個相對比較復雜的過程,更多關于 SSL 握手的過程細節
可以參考 TLS/SSL 握手過程
SSL/TSL 的常見開源實現是 OpenSSL,OpenSSL 是一個開放源代碼的軟件庫包,應用程序
可以使用這個包來進行安全通信,避免竊聽,同時確認另一端連接者的身份。這個包廣泛被
應用在互聯網的網頁服務器上。 更多源于 OpenSSL 的技術細節可以參考 OpenSSL。
2、Http1.1 和 Http1.0 及 2.0 的區別?
HTTP1.0 和 HTTP1.1 的一些區別
HTTP1.0 最早在網頁中使用是在 1996 年,那個時候只是使用一些較為簡單的網頁上和網絡
請求上,而 HTTP1.1 則在 1999 年才開始廣泛應用于現在的各大瀏覽器網絡請求中,同時
HTTP1.1 也是當前使用最為廣泛的 HTTP 協議。 主要區別主要體現在:
1、緩存處理,在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 來做
為緩存判斷的標準,HTTP1.1 則引入了更多的緩存控制策略例如 Entity tag,
If-Unmodified-Since, If-Match, If-None-Match 等更多可供選擇的緩存頭來控制緩
存策略。
2、帶寬優化及網絡連接的使用,HTTP1.0 中,存在一些浪費帶寬的現象,例如客戶
端只是需要某個對象的一部分,而服務器卻將整個對象送過來了,并且不支持斷點
續傳功能,HTTP1.1 則在請求頭引入了 range 頭域,它允許只請求資源的某個部分,
即返回碼是 206(Partial Content),這樣就方便了開發者自由的選擇以便于充分利
用帶寬和連接。
3、錯誤通知的管理,在 HTTP1.1 中新增了 24 個錯誤狀態響應碼,如 409(Conflict)
表示請求的資源與資源的當前狀態發生沖突;410(Gone)表示服務器上的某個資
源被永久性的刪除。
4、Host 頭處理,在 HTTP1.0 中認為每臺服務器都綁定一個唯一的 IP 地址,因此,
請求消息中的 URL 并沒有傳遞主機名(hostname)。但隨著虛擬主機技術的發展,
在一臺物理服務器上可以存在多個虛擬主機(Multi-homed Web Servers),并且它們共享一個 IP 地址。HTTP1.1 的請求消息和響應消息都應支持 Host 頭域,且請求
消息中如果沒有 Host 頭域會報告一個錯誤(400 Bad Request)。
5、長連接,HTTP 1.1 支持長連接(PersistentConnection)和請求的流水線
(Pipelining)處理,在一個 TCP 連接上可以傳送多個 HTTP 請求和響應,減少了建
立和關閉連接的消耗和延遲,在 HTTP1.1 中默認開啟 Connection: keep-alive,一
定程度上彌補了 HTTP1.0 每次請求都要創建連接的缺點。
SPDY
在講 Http1.1 和 Http2.0 的區別之前,還需要說下 SPDY,它是 HTTP1.x 的優化方案:
2012 年 google 如一聲驚雷提出了 SPDY 的方案,優化了 HTTP1.X 的請求延遲,解決了
HTTP1.X 的安全性,具體如下:
1、降低延遲,針對HTTP高延遲的問題,
SPDY優雅的采取了多路復用(multiplexing)。
多路復用通過多個請求 stream 共享一個 tcp 連接的方式,解決了 HOL blocking 的
問題,降低了延遲同時提高了帶寬的利用率。
2、請求優先級(request prioritization)。多路復用帶來一個新的問題是,在連接
共享的基礎之上有可能會導致關鍵請求被阻塞。SPDY 允許給每個 request 設置優先
級,這樣重要的請求就會優先得到響應。比如瀏覽器加載首頁,首頁的 html 內容應
該優先展示,之后才是各種靜態資源文件,腳本文件等加載,這樣可以保證用戶能
第一時間看到網頁內容。
3、header 壓縮。前面提到 HTTP1.x 的 header 很多時候都是重復多余的。選擇合適
的壓縮算法可以減小包的大小和數量。
4、基于 HTTPS 的加密協議傳輸,大大提高了傳輸數據的可靠性。
5、服務端推送(server push),采用了 SPDY 的網頁,例如我的網頁有一個 sytle.css
的請求,在客戶端收到 sytle.css 數據的同時,服務端會將 sytle.js 的文件推送給客戶
端,當客戶端再次嘗試獲取 sytle.js 時就可以直接從緩存中獲取到,不用再發請求了。
SPDY 構成圖:SPDY 位于 HTTP 之下,TCP 和 SSL 之上,這樣可以輕松兼容老版本的 HTTP 協議(將 HTTP1.x
的內容封裝成一種新的 frame 格式),同時可以使用已有的 SSL 功能。
HTTP2.0 和 HTTP1.X 相比的新特性
新的二進制格式(Binary Format),HTTP1.x 的解析是基于文本。基于文本協議的
格式解析存在天然缺陷,文本的表現形式有多樣性,要做到健壯性考慮的場景必然
很多,二進制則不同,只認 0 和 1 的組合。基于這種考慮 HTTP2.0 的協議解析決定
采用二進制格式,實現方便且健壯。
多路復用(MultiPlexing),即連接共享,即每一個 request 都是是用作連接共享機
制的。一個 request 對應一個 id,這樣一個連接上可以有多個 request,每個連接的
request 可以隨機的混雜在一起,接收方可以根據 request 的 id 將 request 再歸屬
到各自不同的服務端請求里面。
header 壓縮,如上文中所言,對前面提到過 HTTP1.x 的 header 帶有大量信息,而
且每次都要重復發送,HTTP2.0 使用 encoder 來減少需要傳輸的 header 大小,通訊
雙方各自 cache 一份 header fields 表,既避免了重復 header 的傳輸,又減小了需
要傳輸的大小。
服務端推送(server push),同 SPDY 一樣,HTTP2.0 也具有 server push 功能。需要更深的理解請點擊這里
3、Https 請求慢的解決辦法
1、不通過 DNS 解析,直接訪問 IP
2、解決連接無法復用
http/1.0 協議頭里可以設置 Connection:Keep-Alive 或者 Connection:Close,選擇是否允許
在一定時間內復用連接(時間可由服務器控制)。但是這對 App 端的請求成效不大,因為
App 端的請求比較分散且時間跨度相對較大。
方案 1.基于 tcp 的長連接 (主要) 移動端建立一條自己的長鏈接通道,通道的實現是基于
tcp 協議。基于 tcp 的 socket 編程技術難度相對復雜很多,而且需要自己定制協議。但信息
的上報和推送變得更及時,請求量爆發的時間點還能減輕服務器壓力(避免頻繁創建和銷毀
連接)
方案 2.http long-polling 客戶端在初始狀態發送一個 polling 請求到服務器,服務器并不會
馬上返回業務數據,而是等待有新的業務數據產生的時候再返回,所以鏈接會一直被保持。
一但結束當前連接,馬上又會發送一個新的 polling 請求,如此反復,保證一個連接被保持。
存在問題: 1)增加了服務器的壓力 2)網絡環境復雜場景下,需要考慮怎么重建健康的
連接通道 3)polling的方式穩定性不好 4)polling的response可能被中間代理cache住 ……
方案 3.http streaming 和 long-polling 不同的是,streaming 方式通過再 server response
的頭部增加“Transfer Encoding:chuncked”來告訴客戶端后續還有新的數據到來 存在問題:
1)有些代理服務器會等待服務器的 response 結束之后才將結果推送給請求客戶端。
streaming 不會結束 response 2)業務數據無法按照請求分割 ……
方案 4.web socket 和傳統的 tcp socket 相似,基于 tcp 協議,提供雙向的數據通道。它的
優勢是提供了 message 的概念,比基于字節流的 tcp socket 使用更簡單。技術較新,不是
所有瀏覽器都提供了支持。
3、解決 head of line blocking
它的原因是隊列的第一個數據包(隊頭)受阻而導致整列數據包受阻
使用 http pipelining,確保幾乎在同一時間把 request 發向了服務器
4、Http 的 request 和 response 的協議組成
1、Request 組成
客戶端發送一個 HTTP 請求到服務器的請求消息包括以下格式:
請求行(request line)、請求頭部(header)、空行和請求數據四個部分組成。請求行以一個方法符號開頭,以空格分開,后面跟著請求的 URI 和協議的版本。
Get 請求例子
GET /562f25980001b1b106000338.jpg HTTP/1.1
Host
img.mukewang.com
User-Agent Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept image/webp,image/*,*/*;q=0.8
Referer http://www.imooc.com/
Accept-Encoding gzip, deflate, sdch
Accept-Language zh-CN,zh;q=0.8
第一部分:請求行,用來說明請求類型,要訪問的資源以及所使用的 HTTP 版本. GET 說明請
求類型為 GET,[/562f25980001b1b106000338.jpg]為要訪問的資源,該行的最后一部分說明
使用的是 HTTP1.1 版本。 第二部分:請求頭部,緊接著請求行(即第一行)之后的部分,
用來說明服務器要使用的附加信息 從第二行起為請求頭部,HOST 將指出請求的目的
地.User-Agent,服務器端和客戶端腳本都能訪問它,它是瀏覽器類型檢測邏輯的重要基礎.該
信息由你的瀏覽器來定義,并且在每個請求中自動發送等等 第三部分:空行,請求頭部后面
的空行是必須的 即使第四部分的請求數據為空,也必須有空行。 第四部分:請求數據也叫
主體,可以添加任意的其他數據。 這個例子的請求數據為空。
POST 請求例子
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR
2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
name=Professional%20Ajax&publisher=Wiley
第一部分:請求行,第一行明了是 post 請求,以及 http1.1 版本。
第二部分:請求頭部,第二行至第六行。
第三部分:空行,第七行的空行。
第四部分:請求數據,第八行。
2、Response 組成
一般情況下,服務器接收并處理客戶端發過來的請求后會返回一個 HTTP 的響應消息。
HTTP 響應也由四個部分組成,分別是:狀態行、消息報頭、空行和響應正文。
第一部分:狀態行,由 HTTP 協議版本號, 狀態碼, 狀態消息 三部分組成。
第一行為狀態行,(HTTP/1.1)表明 HTTP 版本為 1.1 版本,狀態碼為 200,狀態消息為(ok)
第二部分:消息報頭,用來說明客戶端要使用的一些附加信息
第二行和第三行為消息報頭, Date:生成響應的日期和時間;Content-Type:指定了 MIME
類型的 HTML(text/html),編碼類型是 UTF-8
第三部分:空行,消息報頭后面的空行是必須的第四部分:響應正文,服務器返回給客戶端的文本信息。
空行后面的 html 部分為響應正文。
5、談談對 http 緩存的了解。
HTTP 的緩存機制也是依賴于請求和響應 header 里的參數類實現的,最終響應式從緩存中
去,還是從服務端重新拉取,HTTP 的緩存機制的流程如下所示:
HTTP 的緩存可以分為兩種:
強制緩存:需要服務端參與判斷是否繼續使用緩存,當客戶端第一次請求數據是,服務端返
回了緩存的過期時間(Expires 與 Cache-Control),沒有過期就可以繼續使用緩存,否則則
不適用,無需再向服務端詢問。 對比緩存:需要服務端參與判斷是否繼續使用緩存,當客
戶端第一次請求數據時,服務端會將緩存標識(Last-Modified/If-Modified-Since 與
Etag/If-None-Match)與數據一起返回給客戶端,客戶端將兩者都備份到緩存中 ,再次請
求數據時,客戶端將上次備份的緩存 標識發送給服務端,服務端根據緩存標識進行判斷,
如果返回 304,則表示通知客戶端可以繼續使用緩存。 強制緩存優先于對比緩存。
上面提到強制緩存使用的的兩個標識:
Expires:Expires 的值為服務端返回的到期時間,即下一次請求時,請求時間小于服務端返
回的到期時間,直接使用緩存數據。到期時間是服務端生成的,客戶端和服務端的時間可能
有誤差。 Cache-Control:Expires 有個時間校驗的問題,所有 HTTP1.1 采用 Cache-Control
替代 Expires。 Cache-Control 的取值有以下幾種:private: 客戶端可以緩存。 public: 客戶端和代理服務器都可緩存。 max-age=xxx: 緩存的
內容將在 xxx 秒后失效 no-cache: 需要使用對比緩存來驗證緩存數據。 no-store: 所有內
容都不會緩存,強制緩存,對比緩存都不會觸發。 我們再來看看對比緩存的兩個標識:
Last-Modified/If-Modified-Since
Last-Modified 表示資源上次修改的時間。
當客戶端發送第一次請求時,服務端返回資源上次修改的時間:
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
客戶端再次發送,會在 header 里攜帶 If-Modified-Since。將上次服務端返回的資源時間上
傳給服務端。
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT
服務端接收到客戶端發來的資源修改時間,與自己當前的資源修改時間進行對比,如果自己
的資源修改時間大于客戶端發來的資源修改時間,則說明資源做過修改, 則返回 200 表示
需要重新請求資源,否則返回 304 表示資源沒有被修改,可以繼續使用緩存。
上面是一種時間戳標記資源是否修改的方法,還有一種資源標識碼 ETag 的方式來標記是否
修改,如果標識碼發生改變,則說明資源已經被修改,ETag 優先級高于 Last-Modified。
Etag/If-None-Match
ETag 是資源文件的一種標識碼,當客戶端發送第一次請求時,服務端會返回當前資源的標
識碼:
ETag: "5694c7ef-24dc"
客戶端再次發送,會在 header 里攜帶上次服務端返回的資源標識碼:
If-None-Match:"5694c7ef-24dc" 服務端接收到客戶端發來的資源標識碼,則會與自己當前
的資源嗎進行比較,如果不同,則說明資源已經被修改,則返回 200,如果相同則說明資源
沒有被修改,返回 304,客戶端可以繼續使用緩存。
6、Http 長連接。
Http1.0 是短連接,HTTP1.1 默認是長連接,也就是默認 Connection 的值就是 keep-alive。
但是長連接實質是指的 TCP 連接,而不是 HTTP 連接。TCP 連接是一個雙向的通道,它是可
以保持一段時間不關閉的,因此 TCP 連接才有真正的長連接和短連接這一說。
Http1.1 為什么要用使用 tcp 長連接?
長連接是指的 TCP 連接,也就是說復用的是 TCP 連接。即長連接情況下,多個 HTTP 請求
可以復用同一個 TCP 連接,這就節省了很多 TCP 連接建立和斷開的消耗。此外,長連接并不是永久連接的。如果一段時間內(具體的時間長短,是可以在 header 當
中進行設置的,也就是所謂的超時時間),這個連接沒有 HTTP 請求發出的話,那么這個長
連接就會被斷掉。
需要更深的理解請點擊這里
7、Https 加密原理。
加密算法的類型基本上分為了兩種:
?
對稱加密,加密用的密鑰和解密用的密鑰是同一個,比較有代表性的就是 AES 加
密算法;
?
非對稱加密,加密用的密鑰稱為公鑰,解密用的密鑰稱為私鑰,經常使用到的 RSA
加密算法就是非對稱加密的;
此外,還有 Hash 加密算法
HASH 算法:MD5, SHA1, SHA256
相比較對稱加密而言,非對稱加密安全性更高,但是加解密耗費的時間更長,速度慢。
想了解更多加密算法請點擊這里
HTTPS = HTTP + SSL,HTTPS 的加密就是在 SSL 中完成的。
這就要從 CA 證書講起了。CA 證書其實就是數字證書,是由 CA 機構頒發的。至于 CA 機
構的權威性,那么是毋庸置疑的,所有人都是信任它的。CA 證書內一般會包含以下內容:
?
證書的頒發機構、版本
?
證書的使用者
?
證書的公鑰
?
證書的有效時間
?
證書的數字簽名 Hash 值和簽名 Hash 算法
? ...
客戶端如何校驗 CA 證書?
CA 證書中的 Hash 值,其實是用證書的私鑰進行加密后的值(證書的私鑰不在 CA 證書
中)。然后客戶端得到證書后,利用證書中的公鑰去解密該 Hash 值,得到 Hash-a ;然
后再利用證書內的簽名 Hash 算法去生成一個 Hash-b 。最后比較 Hash-a 和 Hash-b 這
兩個的值。如果相等,那么證明了該證書是對的,服務端是可以被信任的;如果不相等,那
么就說明該證書是錯誤的,可能被篡改了,瀏覽器會給出相關提示,無法建立起 HTTPS 連
接。除此之外,還會校驗 CA 證書的有效時間和域名匹配等。HTTPS 中的 SSL 握手建立過程
假設現在有客戶端 A 和服務器 B :
? 1 、 首 先 , 客 戶 端 A 訪 問 服 務 器 B , 比 如 我 們 用 瀏 覽 器 打 開 一 個 網
頁 www.baidu.com ,這時,瀏覽器就是客戶端 A ,百度的服務器就是服務器 B 了。
這時候客戶端 A 會生成一個隨機數 1,把隨機數 1 、自己支持的 SSL 版本號以及
加密算法等這些信息告訴服務器 B 。
? 2、服務器 B 知道這些信息后,然后確認一下雙方的加密算法,然后服務端也生成
一個隨機數 B ,并將隨機數 B 和 CA 頒發給自己的證書一同返回給客戶端 A 。
? 3、客戶端 A 得到 CA 證書后,會去校驗該 CA 證書的有效性,校驗方法在上面
已經說過了。校驗通過后,客戶端生成一個隨機數 3 ,然后用證書中的公鑰加密隨
機數 3 并傳輸給服務端 B 。
? 4、服務端 B 得到加密后的隨機數 3,然后利用私鑰進行解密,得到真正的隨機數
3。
? 5、最后,客戶端 A 和服務端 B 都有隨機數 1、隨機數 2、隨機數 3,然后雙方利
用這三個隨機數生成一個對話密鑰。之后傳輸內容就是利用對話密鑰來進行加解密
了。這時就是利用了對稱加密,一般用的都是 AES 算法。
? 6、客戶端 A 通知服務端 B ,指明后面的通訊用對話密鑰來完成,同時通知服務
器 B 客戶端 A 的握手過程結束。
? 7、服務端 B 通知客戶端 A,指明后面的通訊用對話密鑰來完成,同時通知客戶端
A 服務器 B 的握手過程結束。
? 8、SSL 的握手部分結束,SSL 安全通道的數據通訊開始,客戶端 A 和服務器 B 開
始使用相同的對話密鑰進行數據通訊。
簡化如下:
? 1、客戶端和服務端建立 SSL 握手,客戶端通過 CA 證書來確認服務端的身份;
? 2、互相傳遞三個隨機數,之后通過這隨機數來生成一個密鑰;
? 3、互相確認密鑰,然后握手結束;
? 4、數據通訊開始,都使用同一個對話密鑰來加解密;
可以發現,在 HTTPS 加密原理的過程中把對稱加密和非對稱加密都利用了起來。即利用了
非對稱加密安全性高的特點,又利用了對稱加密速度快,效率高的好處。
需要更深的理解請點擊這里
8、HTTPS 如何防范中間人攻擊?
什么是中間人攻擊?
當數據傳輸發生在一個設備(PC/手機)和網絡服務器之間時,攻擊者使用其技能和工具將
自己置于兩個端點之間并截獲數據;盡管交談的兩方認為他們是在與對方交談,但是實際上
他們是在與干壞事的人交流,這便是中間人攻擊。有幾種攻擊方式?
1、嗅探:嗅探或數據包嗅探是一種用于捕獲流進和流出系統/網絡的數據包的技術。
網絡中的數據包嗅探就好像電話中的監聽。
2、數據包注入: 在這種技術中,攻擊者會將惡意數據包注入常規數據中。這樣用
戶便不會注意到文件/惡意軟件,因為它們是合法通訊流的一部分。
3、會話劫持: 在你登錄進你的銀行賬戶和退出登錄這一段期間便稱為一個會話。
這些會話通常都是黑客的攻擊目標,因為它們包含潛在的重要信息。在大多數案例
中,黑客會潛伏在會話中,并最終控制它。
4、SSL 剝離: 在 SSL 剝離攻擊中,攻擊者使 SSL/TLS 連接剝落,隨之協議便從安
全的 HTTPS 變成了不安全的 HTTP。
HTTPS 如何防范中間人攻擊:
請見 https 加密原理。
9、有哪些響應碼,分別都代表什么意思?
1** 信息,服務器收到請求,需要請求者繼續執行操作
2** 成功,操作被成功接收并處理
3** 重定向,需要進一步的操作以完成請求
4** 客戶端錯誤,請求包含語法錯誤或無法完成請求
5** 服務器錯誤,服務器在處理請求的過程中發生了錯誤
二、TCP/UDP (???)
1、為什么 tcp 要經過三次握手,四次揮手?
重要標志位
ACK : TCP 協議規定,只有 ACK=1 時有效,也規定連接建立后所有發送的報文的 ACK 必
須為 1SYN(SYNchronization) : 在連接建立時用來同步序號。當 SYN=1 而 ACK=0 時,表明這是
一個連接請求報文。對方若同意建立連接,則應在響應報文中使SYN=1和ACK=1. 因此, SYN
置 1 就表示這是一個連接請求或連接接受報文。
FIN (finis)即完,終結的意思, 用來釋放一個連接。當 FIN = 1 時,表明此報文段的發
送方的數據已經發送完畢,并要求釋放連接。
三次握手、四次揮手過程
三次握手:
第一次握手:建立連接。客戶端發送連接請求報文段,將 SYN 位置為 1,Sequence Number
為 x;然后,客戶端進入 SYN_SEND 狀態,等待服務器的確認;
第二次握手:服務器收到 SYN 報文段。服務器收到客戶端的 SYN 報文段,需要對這個 SYN
報文段進行確認,設置 Acknowledgment Number 為 x+1(Sequence Number+1);同時,
自己自己還要發送 SYN 請求信息,將 SYN 位置為 1,Sequence Number 為 y;服務器端將
上述所有信息放到一個報文段(即 SYN+ACK 報文段)中,一并發送給客戶端,此時服務器
進入 SYN_RECV 狀態;
第三次握手:客戶端收到服務器的 SYN+ACK 報文段。然后將 Acknowledgment Number
設置為 y+1,向服務器發送 ACK 報文段,這個報文段發送完畢以后,客戶端和服務器端都
進入 ESTABLISHED 狀態,完成 TCP 三次握手。四次揮手:
第一次分手:主機 1(可以使客戶端,也可以是服務器端),設置 Sequence Number 和
Acknowledgment Number,向主機 2 發送一個 FIN 報文段;此時,主機 1 進入 FIN_WAIT_1
狀態;這表示主機 1 沒有數據要發送給主機 2 了;
第二次分手:主機 2 收到了主機 1 發送的 FIN 報文段,向主機 1 回一個 ACK 報文段,
Acknowledgment Number 為 Sequence Number 加 1;主機 1 進入 FIN_WAIT_2 狀態;主
機 2 告訴主機 1,我“同意”你的關閉請求;
第三次分手:主機 2 向主機 1 發送 FIN 報文段,請求關閉連接,同時主機 2 進入 LAST_ACK
狀態;
第四次分手:主機 1 收到主機 2 發送的 FIN 報文段,向主機 2 發送 ACK 報文段,然后主機
1 進入 TIME_WAIT 狀態;主機 2 收到主機 1 的 ACK 報文段以后,就關閉連接;此時,主機
1 等待 2MSL 后依然沒有收到回復,則證明 Server 端已正常關閉,那好,主機 1 也可以關閉
連接了。
“三次握手”的目的是“為了防止已失效的連接請求報文段突然又傳送到了服務端,因而產生
錯誤”。主要目的防止 server 端一直等待,浪費資源。換句話說,即是為了保證服務端能收
接受到客戶端的信息并能做出正確的應答而進行前兩次(第一次和第二次)握手,為了保證客
戶端能夠接收到服務端的信息并能做出正確的應答而進行后兩次(第二次和第三次)握手。
“四次揮手”原因是因為 tcp 是全雙工模式,接收到 FIN 時意味將沒有數據再發來,但是還是
可以繼續發送數據。
2、TCP 可靠傳輸原理實現(滑動窗口)。確認和重傳:接收方收到報文后就會進行確認,發送方一段時間沒有收到確認就會重傳。
數據校驗。
數據合理分片與排序,TCP 會對數據進行分片,接收方會緩存為按序到達的數據,重新排序
后再提交給應用層。
流程控制:當接收方來不及接收發送的數據時,則會提示發送方降低發送的速度,防止包丟
失。
擁塞控制:當網絡發生擁塞時,減少數據的發送。
關于滑動窗口、流量控制、擁塞控制實現原理請點擊這里
3、Tcp 和 Udp 的區別?
1、基于連接與無連接;
2、對系統資源的要求(TCP 較多,UDP 少);
3、UDP 程序結構較簡單;
4、流模式與數據報模式 ;
5、TCP 保證數據正確性,UDP 可能丟包;
6、TCP 保證數據順序,UDP 不保證。
4、如何設計在 UDP 上層保證 UDP 的可靠性傳輸?
傳輸層無法保證數據的可靠傳輸,只能通過應用層來實現了。實現的方式可以參照 tcp 可靠
性傳輸的方式。如不考慮擁塞處理,可靠 UDP 的簡單設計如下:
? 1、添加 seq/ack 機制,確保數據發送到對端
? 2、添加發送和接收緩沖區,主要是用戶超時重傳。
? 3、添加超時重傳機制。
具體過程即是:送端發送數據時,生成一個隨機 seq=x,然后每一片按照數據大小分配 seq。
數據到達接收端后接收端放入緩存,并發送一個 ack=x 的包,表示對方已經收到了數據。
發送端收到了 ack 包后,刪除緩沖區對應的數據。時間到后,定時任務檢查是否需要重傳數
據。
目前有如下開源程序利用 udp 實現了可靠的數據傳輸。分別為 RUDP、RTP、UDT:
1、RUDP(Reliable User Datagram Protocol)
RUDP 提供一組數據服務質量增強機制,如擁塞控制的改進、重發機制及淡化服務器算法等。2、RTP(Real Time Protocol)
RTP 為數據提供了具有實時特征的端對端傳送服務,如在組播或單播網絡服務下的交互式視
頻音頻或模擬數據。
3、UDT(UDP-based Data Transfer Protocol)
UDT 的主要目的是支持高速廣域網上的海量數據傳輸。
關于 RUDP、RTP、UDT 的更多介紹請查看此處
三、其它重要網絡概念 (??)
1、socket 斷線重連怎么實現,心跳機制又是怎樣實現?
socket 概念
套接字(socket)是通信的基石,是支持 TCP/IP 協議的網絡通信的基本操作單元。它是網
絡通信過程中端點的抽象表示,包含進行網絡通信必須的五種信息:連接使用的協議,本地
主機的 IP 地址,本地進程的協議端口,遠地主機的 IP 地址,遠地進程的協議端口。
為了區別不同的應用程序進程和連接,許多計算機操作系統為應用程序與 TCP/IP 協議交互
提供了套接字(Socket)接口。應 用層可以和傳輸層通過 Socket 接口,區分來自不同應用程
序進程或網絡連接的通信,實現數據傳輸的并發服務。
建立 socket 連接
建立 Socket 連接至少需要一對套接字,其中一個運行于客戶端,稱為 ClientSocket ,另一
個運行于服務器端,稱為 ServerSocket 。
套接字之間的連接過程分為三個步驟:服務器監聽,客戶端請求,連接確認。
?
服務器監聽:服務器端套接字并不定位具體的客戶端套接字,而是處于等待連接的
狀態,實時監控網絡狀態,等待客戶端的連接請求。
?
客戶端請求:指客戶端的套接字提出連接請求,要連接的目標是服務器端的套接字。
為此,客戶端的套接字必須首先描述它要連接的服務器的套接字,指出服務器端- -
套接字的地址和端口號,然后就向服務器端套接字提出連接請求。 連接確認:當服
務器端套接字監聽到或者說接收到客戶端套接字的連接請求時,就響應客戶端套接
字的請求,建立一個新的線程,把服務器端套接字的描述發 給客戶端,一旦客戶端
確認了此描述,雙方就正式建立連接。而服務器端套接字繼續處于監聽狀態,繼續
接收其他客戶端套接字的連接請求。
SOCKET 連接與 TCP 連接創建 Socket 連接時,可以指定使用的傳輸層協議,Socket 可以支持不同的傳輸層協議(TCP
或 UDP),當使用 TCP 協議進行連接時,該 Socket 連接就是一個 TCP 連接。
Socket 連接與 HTTP 連接
由于通常情況下 Socket 連接就是 TCP 連接,因此 Socket 連接一旦建立,通信雙方即可開
始相互發送數據內容,直到雙方連接斷開。但在實際網 絡應用中,客戶端到服務器之間的
通信往往需要穿越多個中間節點,例如路由器、網關、防火墻等,大部分防火墻默認會關閉
長時間處于非活躍狀態的連接而導致 Socket 連接斷連,因此需要通過輪詢告訴網絡,該連
接處于活躍狀態。
而 HTTP 連接使用的是“請求—響應”的方式,不僅在請求時需要先建立連接,而且需要客戶
端向服務器發出請求后,服務器端才能回復數據。
很多情況下,需要服務器端主動向客戶端推送數據,保持客戶端與服務器數據的實時與同步。
此時若雙方建立的是 Socket 連接,服務器就可以直接將數 據傳送給客戶端;若雙方建立的
是 HTTP 連接,則服務器需要等到客戶端發送一次請求后才能將數據傳回給客戶端,因此,
客戶端定時向服務器端發送連接請求, 不僅可以保持在線,同時也是在“詢問”服務器是否
有新的數據,如果有就將數據傳給客戶端。TCP(Transmission Control Protocol) 傳輸控制
協議
socket 斷線重連實現
正常連接斷開客戶端會給服務端發送一個 fin 包,服務端收到 fin 包后才會知道連接斷開。而
斷網斷電時客戶端無法發送 fin 包給服務端,所以服務端沒辦法檢測到客戶端已經短線。 為
了緩解這個問題,服務端需要有個心跳邏輯,就是服務端檢測到某個客戶端多久沒發送任何
數據過來就認為客戶端已經斷開, 這需要客戶端定時向服務端發送心跳數據維持連接。
心跳機制實現
長連接的實現:心跳機制,應用層協議大多都有 HeartBeat 機制,通常是客戶端每隔一小段
時間向服務器發送一個數據包,通知服務器自己仍然在線。并傳輸一些可能必要的數據。使
用心跳包的典型協議是 IM,比如 QQ/MSN/飛信等協議
1、在 TCP 的機制里面,本身是存在有心跳包的機制的,也就是 TCP 的選項:SO_KEEPALIVE。
系統默認是設置的 2 小時的心跳頻率。但是它檢查不到機器斷電、網線拔出、防火墻這些斷
線。 而且邏輯層處理斷線可能也不是那么好處理。一般,如果只是用于保活還是可以的。
通過使用 TCP 的 KeepAlive 機制(修改那個 time 參數),可以讓連接每隔一小段時間就產
生一些 ack 包,以降低被踢掉的風險,當然,這樣的代價是額外的網絡和 CPU 負擔。
2、應用層心跳機制實現。
2、Cookie 與 Session 的作用和原理。? Session 是在服務端保存的一個數據結構,用來跟蹤用戶的狀態,這個數據可以保存
在集群、數據庫、文件中。
? Cookie 是客戶端保存用戶信息的一種機制,用來記錄用戶的一些信息,也是實現
Session 的一種方式。
Session:
由于 HTTP 協議是無狀態的協議,所以服務端需要記錄用戶的狀態時,就需要用某種機制來
識具體的用戶,這個機制就是 Session.典型的場景比如購物車,當你點擊下單按鈕時,由于
HTTP 協議無狀態,所以并不知道是哪個用戶操作的,所以服務端要為特定的用戶創建了特
定的 Session,用用于標識這個用戶,并且跟蹤用戶,這樣才知道購物車里面有幾本書。這
個 Session 是保存在服務端的,有一個唯一標識。在服務端保存 Session 的方法很多,內存、
數據庫、文件都有。集群的時候也要考慮 Session 的轉移,在大型的網站,一般會有專門的
Session 服務器集群,用來保存用戶會話,這個時候 Session 信息都是放在內存的。
具體到 Web 中的 Session 指的就是用戶在瀏覽某個網站時,從進入網站到瀏覽器關閉所經
過的這段時間,也就是用戶瀏覽這個網站所花費的時間。因此從上述的定義中我們可以看到,
Session 實際上是一個特定的時間概念。
當客戶端訪問服務器時,服務器根據需求設置 Session,將會話信息保存在服務器上,同時
將標示 Session 的 SessionId 傳遞給客戶端瀏覽器,
瀏覽器將這個 SessionId 保存在內存中,我們稱之為無過期時間的 Cookie。瀏覽器關閉后,
這個 Cookie 就會被清掉,它不會存在于用戶的 Cookie 臨時文件。
以后瀏覽器每次請求都會額外加上這個參數值,服務器會根據這個 SessionId,就能取得客
戶端的數據信息。
如果客戶端瀏覽器意外關閉,服務器保存的 Session 數據不是立即釋放,此時數據還會存在,
只要我們知道那個 SessionId,就可以繼續通過請求獲得此 Session 的信息,因為此時后臺的
Session 還存在,當然我們可以設置一個 Session 超時時間,一旦超過規定時間沒有客戶端
請求時,服務器就會清除對應 SessionId 的 Session 信息。
Cookie
Cookie 是由服務器端生成,發送給 User-Agent(一般是 web 瀏覽器),瀏覽器會將 Cookie
的 key/value 保存到某個目錄下的文本文件內,下次請求同一網站時就發送該 Cookie 給服
務器(前提是瀏覽器設置為啟用 Cookie)。Cookie 名稱和值可以由服務器端開發自己定義,
對于 JSP 而言也可以直接寫入 Sessionid,這樣服務器可以知道該用戶是否合法用戶以及是
否需要重新登錄等。
3、IP 報文中的內容。版本:IP 協議的版本,目前的 IP 協議版本號為 4,下一代 IP 協議版本號為 6。
首部長度:IP 報頭的長度。固定部分的長度(20 字節)和可變部分的長度之和。共占 4 位。
最大為 1111,即 10 進制的 15,代表 IP 報頭的最大長度可以為 15 個 32bits(4 字節),也
就是最長可為 15*4=60 字節,除去固定部分的長度 20 字節,可變部分的長度最大為 40 字
節。
服務類型:Type Of Service。
總長度:IP 報文的總長度。報頭的長度和數據部分的長度之和。
標識:唯一的標識主機發送的每一分數據報。通常每發送一個報文,它的值加一。當 IP 報
文長度超過傳輸網絡的 MTU(最大傳輸單元)時必須分片,這個標識字段的值被復制到所
有數據分片的標識字段中,使得這些分片在達到最終目的地時可以依照標識字段的內容重新
組成原先的數據。
標志:共 3 位。R、DF、MF 三位。目前只有后兩位有效,DF 位:為 1 表示不分片,為 0
表示分片。MF:為 1 表示“更多的片”,為 0 表示這是最后一片。
片位移:本分片在原先數據報文中相對首位的偏移位。(需要再乘以 8)
生存時間:IP 報文所允許通過的路由器的最大數量。每經過一個路由器,TTL 減 1,當為 0
時,路由器將該數據報丟棄。TTL 字段是由發送端初始設置一個 8 bit 字段.推薦的初始值由
分配數字 RFC 指定,當前值為 64。發送 ICMP 回顯應答時經常把 TTL 設為最大值 255。協議:指出 IP 報文攜帶的數據使用的是那種協議,以便目的主機的 IP 層能知道要將數據報
上交到哪個進程(不同的協議有專門不同的進程處理)。和端口號類似,此處采用協議號,
TCP 的協議號為 6,UDP 的協議號為 17。ICMP 的協議號為 1,IGMP 的協議號為 2.
首部校驗和:計算 IP 頭部的校驗和,檢查 IP 報頭的完整性。
源 IP 地址:標識 IP 數據報的源端設備。
目的 IP 地址:標識 IP 數據報的目的地址。
最后就是可變部分和數據部分。
四、常見網絡流程機制 (??)
1、瀏覽器輸入地址到返回結果發生了什么?
總體來說分為以下幾個過程:
1、DNS 解析,此外還有 DNSy 優化(DNS 緩存、DNS 負載均衡)
2、TCP 連接
3、發送 HTTP 請求
4、服務器處理請求并返回 HTTP 報文
5、瀏覽器解析渲染頁面
6、連接結束
Web 前端的本質
將信息快速并友好的展示給用戶并能夠與用戶進行交互。
如何盡快的加載資源(網絡優化)?
答案就是能不從網絡中加載的資源就不從網絡中加載,當我們合理使用緩存,將資源放在瀏
覽器端,這是最快的方式。如果資源必須從網絡中加載,則要考慮縮短連接時間,即 DNS
優化部分;減少響應內容大小,即對內容進行壓縮。另一方面,如果加載的資源數比較少的
話,也可以快速的響應用戶。
第二節、操作系統面試題 (???)
1、操作系統如何管理內存的?2、進程調度。
3、說下 Linux 進程和線程的區別。
進程和線程的主要差別在于它們是不同的操作系統資源管理方式。進程有獨立的
地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只
是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有
單獨的地址空間,一個線程死掉就等于整個進程死掉,所以多進程的程序要比多
線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對于一些
要求同時進行并且又要共享某些變量的并發操作,只能用線程,不能用進程。
簡而言之,一個程序至少有一個進程,一個進程至少有一個線程。
線程的劃分尺度小于進程,使得多線程程序的并發性高。
另外,進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從
而極大地提高了程序的運行效率。
線程在執行過程中與進程還是有區別的。每個獨立的線程有一個程序運行
的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依
存在應用程序中,由應用程序提供多個線程執行控制。
從邏輯角度來看,多線程的意義在于一個應用程序中,有多個執行部分可
以同時執行。但操作系統并沒有將多個線程看做多個獨立的應用,來實現
進程的調度和管理以及資源分配。這就是進程和線程的重要區別。
4、你能解釋一下 Linux 的軟鏈接和硬鏈接嗎?
Linux 鏈接分兩種,一種被稱為硬鏈接(Hard Link),另一種被稱為符號鏈接
(Symbolic Link)。默認情況下,ln 命令產生硬鏈接。
硬連接
硬連接指通過索引節點來進行連接。在 Linux 的文件系統中,保存在磁盤分區中
的文件不管是什么類型都給它分配一個編號,稱為索引節點號(Inode Index)。在
Linux 中,多個文件名指向同一索引節點是存在的。一般這種連接就是硬連接。
硬連接的作用是允許一個文件擁有多個有效路徑名,這樣用戶就可以建立硬連接到重要文件,以防止“誤刪”的功能。其原因如上所述,因為對應該目錄的索引節
點有一個以上的連接。只刪除一個連接并不影響索引節點本身和其它的連接,只
有當最后一個連接被刪除后,文件的數據塊及目錄的連接才會被釋放。也就是說,
文件真正刪除的條件是與之相關的所有硬連接文件均被刪除。
軟連接
另外一種連接稱之為符號連接(Symbolic Link),也叫軟連接。軟鏈接文件有類
似于 Windows 的快捷方式。它實際上是一個特殊的文件。在符號連接中,文件
實際上是一個文本文件,其中包含的有另一文件的位置信息。
5、安卓權限管理,為何在清單中注冊權限,安卓 APP 就可以使用,反之不可
以?
此題考查 Android 的權限管理在 Android 的安全架構中的作用。
Android 是一個權限分隔的操作系統,其中每個應用都有其獨特的系統標識
(Linux 用戶 ID 和組 ID)。系統各部分也分隔為不同的標識。Linux 據此將不
同的應用以及應用與系統分隔開來。
其他更詳細的安全功能通過“權限”機制提供,此機制會限制特定進程可以執行的
具體操作,并且根據 URI 權限授權臨時訪問特定的數據段。
Android 安全架構的中心設計點是:在默認情況下任何應用都沒有權限執行對其
他應用、操作系統或用戶有不利影響的任何操作。這包括讀取或寫入用戶的私有
數據(例如聯系人或電子郵件)、讀取或寫入其他應用程序的文件、執行網絡訪
問、使設備保持喚醒狀態等。
由于每個 Android 應用都是在進程沙盒中運行,因此應用必須顯式共享資源和
數據。它們的方法是聲明需要哪些權限來獲取基本沙盒未提供的額外功能。應用
以靜態方式聲明它們需要的權限,然后 Android 系統提示用戶同意。
第三節、數據庫面試題 (?)1、數據庫的四大特征,數據庫的隔離級別?
事務(Transaction)是并發控制的基本單位。所謂的事務,它是一個操作序列,
這些操作要么都執行,要么都不執行,它是一個不可分割的工作單位。例如,銀
行轉賬工作:從一個賬號扣款并使另一個賬號增款,這兩個操作要么都執行,要
么都不執行。所以,應該把它們看成一個事務。事務是數據庫維護數據一致性的
單位,在每個事務結束時,都能保持數據一致性。事務具有以下 4 個基本特征:
數據庫的四大特征:
(1)原子性(Atomicity)
原子性是指事務包含的所有操作要么全部成功,要么全部失敗回滾。
(2)一致性(Consistency)
一個事務執行之前和執行之后都必須處于一致性狀態。
(3)隔離性(Isolation)
隔離性是當多個用戶并發訪問數據庫時,比如操作同一張表時,數據庫為每一個
用戶開啟的事務,不能被其他事務的操作所干擾,多個并發事務之間要相互隔離。
(4)持久性(Durability)
持久性是指一個事務一旦被提交了,那么對數據庫中的數據的改變就是永久性
的。
數據庫的隔離級別:
1)Serializable(串行化):可避免臟讀、不可重復讀、幻讀的發生。
2)Repeatable read (可重復讀):可避免臟讀、不可重復讀的發生。
3)Read committed (讀已提交):可避免臟讀的發生。4)Read uncommitted (讀未提交):最低級別,任何情況都無法保證。
2、數據庫設計中常講的三范式是指什么?
1)第一范式 1NF(域的原子性)
如果數據庫表中的所有字段值都是不可分解的原子值,就說明該數據庫表滿足了
第一范式
2)第二范式 2NF(表中除主鍵外的字段都完全依賴主鍵)
第二范式是在第一范式基礎上建立的。第二范式有兩個重點:(1)表中必須有主鍵;
(2)其他非主屬性必須完全依賴主鍵,不能只依賴主鍵的一部分(主要針對聯合
主鍵而言)。
3)第三范式 3NF(表中除主鍵外的字段都完全直接依賴,不能是傳遞依賴)
不能是傳遞依賴,即不能存在:非主鍵列 A 依賴于非主鍵列 B,非主鍵列 B 依
賴于主鍵的情況。第二范式和第三范式區分的關鍵點:2NF:非主鍵列是否完全
依賴于主鍵,還是依賴于主鍵的一部分;3NF:非主鍵列是直接依賴于主鍵,還
是直接依賴于非主鍵列。
第二章 數據結構和算法面試題
數據結構與算法
對于算法面試準備,無疑就是刷《劍指 Offer》+ LeetCode 效果最佳。刷《劍
指 Offer》是為了建立全面的算法面試思維,打下堅實的基礎,刷 LeetCode 則
是為了不斷強化與開闊我們自己的算法思想。這兩塊 CS-Notes 中已經實現地很完美了,建議大家將《劍指 Offer》刷完,然后再至少刷 100 道 LeetCode 題目
以上。
1、劍指 Offer 題解
2、Leetcode 題解
建議刷完《劍指 Offer》+ 至少 100 道 LeetCode 題目后,再去進
一步看看下面的高頻題目有哪些沒有刷到。
一、高頻題集 (???)
1、無重復字符的最長子串
2、簡化路徑
3、復原 IP 地址
4、三數之和
5、島嶼的最大面積
6、搜索旋轉排序數組
7、朋友圈
8、接雨水
9、反轉鏈表
10、兩數相加
11、合并兩個有序鏈表
12、合并 K 個排序鏈表
13、買賣股票的最佳時機
14、買賣股票的最佳時機 II
15、最大子序和16、最小棧
17、LRU 緩存機制
18、尋找兩個有序數組的中位數
19、最長回文子串
20、合并兩個有序數組
21、整數反轉
22、排序鏈表
23、子集
24、全排列
25、實現二叉樹中序遍歷(不使用遞歸)
26、爬樓梯(斐波那契數列)
27、滑動窗口的最大值
28、判斷單鏈表成環與否?
29、如何從一百萬個數里面找到最小的一百個數,考慮算法的時間復雜度和空間復雜度
30、手寫數組實現隊列
31、java 排序算法和查找算法 (寫出你所知道的排序算法及時空復雜度,穩定性)
http://www.jianshu.com/p/8c915179fd02
http://xiaojun-it.iteye.com/blog/2291852
二、次高頻題集 (??)
1、算法熟悉么?給了一個二叉排序樹,出了一個給定節點找到它的下一個元素(指的是大小順
序的下一個)的算法題。
2、x 個蘋果,一天只能吃一個、兩個、或者三個,問多少天可以吃完
3、求二叉樹第 n 層節點數4、如何設計一個抽獎系統,比如滿 200 抽 20,滿 500 抽 50。
5、求無序數組中的中位數
6、二叉樹深度算法
7、堆和棧在內存中的區別是什么(數據結構方面以及實際實現方面)
8、最快的排序算法是哪個?給阿里 2 萬多名員工按年齡排序應該選擇哪個算法?
9、堆和樹的區別?
10、求 1000 以內的水仙花數以及 40 億以內的水仙花數;
11、子串包含問題(KMP 算法)寫代碼實現;
12、萬億級別的兩個 URL 文件 A 和 B,如何求出 A 和 B 的差集 C,(Bit 映射->hash 分組->多文件
讀寫效率->磁盤尋址以及應用層面對尋址的優化)
13、蟻群算法與蒙特卡洛算法;
14、百度 POI 中如何試下查找最近的商家功能(坐標鏡像+R 樹)。
15、5 枚硬幣,2 正 3 反如何劃分為兩堆然后通過翻轉讓兩堆中正面向上的硬幣和反面向上的硬
幣個數相同;
16、時針走一圈,時針分針重合幾次;
17、N * N 的方格紙,里面有多少個正方形;
18、請在 100 個電話號碼找出 135 的電話號碼 注意 不能用正則,(類似怎么最好的遍歷 LogGat
日志)
19、一個青蛙跳臺階,一次可以跳一步和兩步,如果一共有 N 個臺階,可以有幾種跳法?
20、寫代碼實現隊列的基本操作,外加查找最大值;
21、圖:有向無環圖的解釋
22、二叉樹 深度遍歷與廣度遍歷
23、B 樹、B+樹
24、密碼學中兩大加密算法是什么
25、判斷環(猜測應該是鏈表環)26、有一個 List 列表,去掉列表中的某一 Object 對象,如何在 for 循環里面寫;
27、設計移動端的聯系人存儲與查詢的功能,要求快速搜索聯系人,可以用到哪些數據結構?
(二叉排序樹,建立索引)
28、一道簡單不易的算法題
int a = 10;
int b=5;
怎么在不引入其他變量的情況下,讓 a 和 b 互換?
```
public class Test {
int a = 10;
int b=5;
public static void main(String[] args) {
a = a+b;
b=a-b;
a =a-b;
System.out.println("b="+b);
System.out.println("a="+a);
}
}
----輸出:
b=10
a=5
```
29、回形打印二維數組30、二叉樹,給出根節點和目標節點,找出從根節點到目標節點的路徑
31、一個無序,不重復數組,輸出 N 個元素,使得 N 個元素的和相加為 M,給出時間復雜度、空
間復雜度。手寫算法
32、兩個不重復的數組集合中,求共同的元素。
33、上一問擴展,海量數據,內存中放不下,怎么求出。
34、從長度為 m 的 int 數組中隨機取出 n 個元素,每次取的元素都是之前未取過的,如何優化
35、逆序一個字符串,不能調用 String 的 reverse 方法(考察編碼風格)
36、算法:將一個有序數組去重得到一個新數組(空間復雜度為 O(N))
37、算法:如何從 1T 的無序數組(長度為 n)里面找出前 k 大的數據,復雜度要求為 O(logN)
38、m * n 的矩陣,能形成幾個正方形(2 * 2 能形成 1 個正方形,2 * 3 2 個,3 * 3 6 個)
計數的關鍵是要觀察到任意一個傾斜的正方形必然唯一內接于一個非傾斜的正方形,而一個非傾
斜的邊長為 k 的非傾斜正方形,一條邊上有 k-1 個內點,每個內點恰好確定一個內接于其中的
傾斜正方形,加上非傾斜正方形本身,可知,將邊長為 k 的非傾斜正方形數目乘以 k,再按 k 求
和即可得到所有正方形的數目。
設 2≤n≤m,k≤n-1,則邊長為 k 的非傾斜有
(n-k)(m-k)個,故所有正方形有
∑(m-k)(n-k)k 個
例如 m=n=4
正方形有 331+222+113=20 個。
39、面試頭條的時候在線編程:從上到下從左到右輸出二叉樹40、從長度為 m 的 int 數組中隨機取出 n 個元素,每次取的元素都是之前未取過的,如何優化
41、逆序一個字符串,不能調用 String 的 reverse 方法(考察編碼風格)
42、算法:將一個有序數組去重得到一個新數組(空間復雜度為 O(N))
43、算法:如何從 1T 的無序數組(長度為 n)里面找出前 k 大的數據,復雜度要求為 O(logN)
44、堆和樹的區別
45、堆和棧在內存中的區別是什么(解答提示:可以從數據結構方面以及實際實現方面兩個方面
去回答)?
46、什么是深拷貝和淺拷貝
47、手寫鏈表逆序代碼
48、講一下對圖的理解
49、手寫一段代碼,如何找出一段字符串中,出現最多的漢字是哪個?
50、單向鏈表逆序。
51、實現一個數組插入。(處理異常判別,不使用 Collections 相關接口)。
52、找到無序數組的最大連續求和。
53、找到多個員工的共同繁忙時段。
https://github.com/banking/algorithm-dish/blob/master/algorithm-question/
src/main/java/TimeAirbnb.java
54、輸出一個集合{A,B,C,D}的全部子集**
55、手寫代碼:遍歷文件目錄;
56、電梯運行的算法分析;
57、手寫實現單鏈表的 get 操作;
58、100 個數字排序怎么做?
59、一個集合里有 1000 萬個隨機元素,如何快速計算他們的和(我特喵的以為是考算法,想半
天沒有 O(n)以下的方案,結果他居然說多線程)60、切餅問題:1 刀切 2 塊,2 刀切 4 塊,10 刀最多切幾塊。
61、追擊問題:2 輛火車相向同時出發,一個從 A 點出發,速度是 30km/h,一個從 B 點出發,速
度是 20km/h,A、B 之間的距離是 S,同時一只鳥也從 A 點出發,速度是 50km/h,鳥在兩輛火車
間折返飛行,問三者相遇時,鳥飛的總路程。
62、算法:給定一段已排好序的數列,數列中元素有可能是重復的,找出數列中目標值的最小
索引,要求不能用遞歸,只能用循環。
63、一個文件中有 100 萬個整數,由空格分開,在程序中判斷用戶輸入的整數是否在此文件中。
說出最優的方法
64、2000 萬個整數,找出第五十大的數字?
65、燒一根不均勻的繩,從頭燒到尾總共需要 1 個小時。現在有若干條材質相同的繩子,問如
何用燒繩的方法來計時一個小時十五分鐘呢?
66、求 1000 以內的水仙花數以及 40 億以內的水仙花數
67、稱重問題:10 個箱子,每個箱子里都有若干磚頭,其中有一個箱子里每個磚頭重 9 克,其
他的箱子里的磚頭每個重 10 克,給你一個秤,要求只能用一次稱重找出磚頭重 9 克的箱子。
68、時針走一圈,時針分針重合幾次
69、N*N 的方格紙,里面有多少個正方形
70、標號 1-n 的 n 個人首尾相接,1 到 3 報數,報到 3 的退出,求最后一個人的標號
71、給定一個字符串,求第一個不重復的字符 abbcad -> c
72、上網站寫代碼,如下: 有一個容器類 ArrayList,保存整數類型的元素,現在要求編寫一
個幫助類,類內提供一個幫助函數,幫助函數的功能是刪除 容器中<10 的元素。
73、寫代碼,LeetCode 上股票利益最大化問題
74、寫代碼,劍指 offer 上第一次只出現一次的字符
75、算法:連續子數組的最大和
76、子串包含問題(KMP 算法)寫代碼實現
77、ViewGroup 的層級深度,轉換為二叉樹的層級深度
78、String 字符串的數字相加
79、使用三個線程順序打印有序的數組80、給定一個有序的數組和目標數,找出與目標數最近接的 index,要求復雜度是 log(n)的時
間復雜度
81、給定一個二叉樹和一個目標數,在二叉樹中是否存在一條路徑的所有節點的和與目標數是
相同的 case,并且打印。
82、二叉樹,讀取每一層最右邊的節點
83、int 數組,除了一個數字外,其他數字都出現兩次,找出這個只出現一次的數字
84、一個類,內部有一個鏈表的數據結構,實現 void add(Node n)和 void remove(int index)
的函數
85、手寫消費者生產者模型的代碼
86、一個無序的 int 數組,給一個 target 數字,找出數組中兩個數字相加為 target,并輸出坐
87、數組中存有 1-3 的三種數字,例如[1,2,3,1,2,2,1,3,3],將其排序為[1,1,1,2,2,2,3,3,3],
要求時間復雜度,后續將內容變為一個對象,繼續排序
88、"之"字形打印二叉樹
89、1~100 盞燈,都是亮的,第一次將能被 1 整除的數的燈按下,變暗,第二次將能被 2 整除的
數的等按下,變亮,第三次將能被 3 整除的數的等按下,變暗…第 100 次將能被 100 整除的數
的燈按下,問,最后有多少盞燈是亮的。
90、實現一個 o(n)復雜度的堆和最大數。
91、實現一個數組的窗口掃描算法。
92、識別一個字符串是否是 ipv4 地址。
93、o(n)復雜度實現偶數遞增奇數遞減單向鏈接排序。
第三章 Java 面試題
第一節 Java 基礎面試題
一、面向對象 (???)
1、談談對 java 多態的理解?多態是指父類的某個方法被子類重寫時,可以產生自己的功能行為,同一個操作
作用于不同對象,可以有不同的解釋,產生不同的執行結果。
多態的三個必要條件:
?
繼承父類。
?
重寫父類的方法。
?
父類的引用指向子類對象。
什么是多態
面向對象的三大特性:封裝、繼承、多態。從一定角度來看,封裝和繼承幾乎都
是為多態而準備的。這是我們最后一個概念,也是最重要的知識點。
多態的定義:指允許不同類的對象對同一消息做出響應。即同一消息可以根據發
送對象的不同而采用多種不同的行為方式。(發送消息就是函數調用)
實現多態的技術稱為:動態綁定(dynamic binding),是指在執行期間判斷所
引用對象的實際類型,根據其實際的類型調用其相應的方法。
多態的作用:消除類型之間的耦合關系。
現實中,關于多態的例子不勝枚舉。比方說按下 F1 鍵這個動作,如果當前在
Flash 界面下彈出的就是 AS 3 的幫助文檔;如果當前在 Word 下彈出的就是
Word 幫助;在 Windows 下彈出的就是 Windows 幫助和支持。同一個事件發
生在不同的對象上會產生不同的結果。
多態的好處:
1.可替換性(substitutability)。多態對已存在代碼具有可替換性。例如,多態
對圓 Circle 類工作,對其他任何圓形幾何體,如圓環,也同樣工作。
2.可擴充性(extensibility)。多態對代碼具有可擴充性。增加新的子類不影響已
存在類的多態性、繼承性,以及其他特性的運行和操作。實際上新加子類更容易
獲得多態功能。例如,在實現了圓錐、半圓錐以及半球體的多態基礎上,很容易
增添球體類的多態性。3.接口性(interface-ability)。多態是超類通過方法簽名,向子類提供了一個共
同接口,由子類來完善或者覆蓋它而實現的。
4.靈活性(flexibility)。它在應用中體現了靈活多樣的操作,提高了使用效率。
5.簡化性(simplicity)。多態簡化對應用軟件的代碼編寫和修改過程,尤其在處
理大量對象的運算和操作時,這個特點尤為突出和重要。
Java 中多態的實現方式:接口實現,繼承父類進行方法重寫,同一個類中進行方
法重載。
2、你所知道的設計模式有哪些?
答:Java 中一般認為有 23 種設計模式,我們不需要所有的都會,但是其中常用
的種設計模式應該去掌握。下面列出了所有的設計模式。要掌握的設計模式我單
獨列出來了,當然能掌握的越多越好。
總體來說設計模式分為三大類:
創建型模式,共五種:
工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。
結構型模式,共七種:
適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模
式。
行為型模式,共十一種:
策略模式、模板方法模式、觀者模式、迭代子模式、責任鏈模式、命令模式、備
忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。
具體可見我的設計模式總結筆記
3、通過靜態內部類實現單例模式有哪些優點?
1. 不用 synchronized ,節省時間。
2. 調用 getInstance() 的時候才會創建對象,不調用不創建,節省空間,這
有點像傳說中的懶漢式。4、靜態代理和動態代理的區別,什么場景使用?
靜態代理與動態代理的區別在于代理類生成的時間不同,即根據程序運行前代理
類是否已經存在,可以將代理分為靜態代理和動態代理。如果需要對多個類進行
代理,并且代理的功能都是一樣的,用靜態代理重復編寫代理類就非常的麻煩,
可以用動態代理動態的生成代理類。
// 為目標對象生成代理對象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("開啟事務");
// 執行目標對象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事務");
return null;
}
});
}
?
靜態代理使用場景:四大組件同 AIDL 與 AMS 進行跨進程通信
?
動態代理使用場景:Retrofit 使用了動態代理極大地提升了擴展性和可維
護性。5、簡單工廠、工廠方法、抽象工廠、Builder 模式的區別?
?
簡單工廠模式:一個工廠方法創建不同類型的對象。
?
工廠方法模式:一個具體的工廠類負責創建一個具體對象類型。
?
抽象工廠模式:一個具體的工廠類負責創建一系列相關的對象。
? Builder 模式:對象的構建與表示分離,它更注重對象的創建過程。
6、裝飾模式和代理模式有哪些區別 ?與橋接模式相比呢?
? 1、裝飾模式是以客戶端透明的方式擴展對象的功能,是繼承關系的一個
替代方案;而代理模式則是給一個對象提供一個代理對象,并由代理對象
來控制對原有對象的引用。
? 2、裝飾模式應該為所裝飾的對象增強功能;代理模式對代理的對象施加
控制,但不對對象本身的功能進行增加。
? 3、橋接模式的作用于代理、裝飾截然不同,它主要是為了應對某個類族
有多個變化維度導致子類類型急劇增多的場景。通過橋接模式將多個變化
維度隔離開,使得它們可以獨立地變化,最后通過組合使它們應對多維變
化,減少子類的數量和復雜度。
7、外觀模式和中介模式的區別?
外觀模式重點是對外封裝統一的高層接口,便于用戶使用;而中介模式則是避免
多個互相協作的對象直接引用,它們之間的交互通過一個中介對象進行,從而使
得它們耦合松散,能夠易于應對變化。
8、策略模式和狀態模式的區別?
雖然兩者的類型結構是一致的,但是它們的本質卻是不一樣的。策略模式重在整
個算法的替換,也就是策略的替換,而狀態模式則是通過狀態來改變行為。
9、適配器模式,裝飾者模式,外觀模式的異同?
這三個模式的相同之處是,它們都作用于用戶與真實被使用的類或系統之間,作
一個中間層,起到了讓用戶間接地調用真實的類的作用。它們的不同之外在于,
如上所述的應用場合不同和本質的思想不同。
代理與外觀的主要區別在于,代理對象代表一個單一對象,而外觀對象代表一個
子系統,代理的客戶對象無法直接訪問對象,由代理提供單獨的目標對象的訪問,
而通常外觀對象提供對子系統各元件功能的簡化的共同層次的調用接口。代理是
一種原來對象的代表,其它需要與這個對象打交道的操作都是和這個代表交涉的。而適配器則不需要虛構出一個代表者,只需要為應付特定使用目的,將原來
的類進行一些組合。
外觀與適配器都是對現存系統的封裝。外觀定義的新的接口,而適配器則是復用
一個原有的接口,適配器是使兩個已有的接口協同工作,而外觀則是為現存系統
提供一個更為方便的訪問接口。如果硬要說外觀是適配,那么適配器有用來適配
對象的,而外觀是用來適配整個子系統的。也就是說,外觀所針對的對象的粒度
更大。
代理模式提供與真實的類一致的接口,意在用代理類來處理真實的類,實現一些
特定的服務或真實類的部分功能,Facade(外觀)模式注重簡化接口,Adapter
(適配器)模式注重轉換接口。
10、代碼的壞味道:
1、代碼重復:
代碼重復幾乎是最常見的異味了。他也是 Refactoring 的主要目標之一。代碼重
復往往來自于 copy-and-paste 的編程風格。
2、方法過長:
一個方法應當具有自我獨立的意圖,不要把幾個意圖放在一起。
3、類提供的功能太多:
把太多的責任交給了一個類,一個類應該僅提供一個單一的功能。
4、數據泥團:
某些數據通常像孩子一樣成群玩耍:一起出現在很多類的成員變量中,一起出現
在許多方法的參數中…..,這些數據或許應該自己獨立形成對象。 比如以單例的
形式對外提供自己的實例。
5、冗贅類:
一個干活不多的類。類的維護需要額外的開銷,如果一個類承擔了太少的責任,
應當消除它。6、需要太多注釋:
經常覺得要寫很多注釋表示你的代碼難以理解。如果這種感覺太多,表示你需要
Refactoring。
11、是否能從 Android 中舉幾個例子說說用到了什么設計模式 ?
AlertDialog、Notification 源碼中使用了 Bulider(建造者)模式完成參數的初始化:
在 AlertDialog 的 Builder 模式中并沒有看到 Direcotr 角色的出現,其實在很多場
景中,Android 并沒有完全按照 GOF 的經典設計模式來實現,而是做了一些修
改,使得這個模式更易于使用。這個的 AlertDialog.Builder 同時扮演了上下文中
提到的 builder、ConcreteBuilder、Director 的角色,簡化了 Builder 模式的設計。
當模塊比較穩定,不存在一些變化時,可以在經典模式實現的基礎上做出一些精
簡,而不是照搬 GOF 上的經典實現,更不要生搬硬套,使程序失去架構之美。
定義:將一個復雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不
同的表示。即將配置從目標類中隔離出來,避免過多的 setter 方法。
優點:
? 1、良好的封裝性,使用建造者模式可以使客戶端不必知道產品內部組成
的細節。
? 2、建造者獨立,容易擴展。
缺點:
?
會產生多余的 Builder 對象以及 Director 對象,消耗內存。
日常開發的 BaseActivity 抽象工廠模式:
定義:為創建一組相關或者是相互依賴的對象提供一個接口,而不需要指定它們
的具體類。
主題切換的應用:比如我們的應用中有兩套主題,分別為亮色主題 LightTheme 和暗色主題
DarkTheme,這兩種主題我們可以通過一個抽象的類或接口來定義,而在對應主
題下我們又有各類不同的 UI 元素,比如 Button、TextView、Dialog、ActionBar
等,這些 UI 元素都會分別對應不同的主題,這些 UI 元素我們也可以通過抽象的
類或接口定義,抽象的主題、具體的主題、抽象的 UI 元素和具體的 UI 元素之間
的關系就是抽象工廠模式最好的體現。
優點:
?
分離接口與實現,面向接口編程,使其從具體的產品實現中解耦,同時基
于接口與實現的分離,使抽象該工廠方法模式在切換產品類時更加靈活、
容易。
缺點:
?
類文件的爆炸性增加。
?
新的產品類不易擴展。
Okhttp 內部使用了責任鏈模式來完成每個 Interceptor 攔截器的調用:
定義:使多個對象都有機會處理請求,從而避免了請求的發送者和接收者之間的
耦合關系。將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有對象處理
它為止。
ViewGroup 事件傳遞的遞歸調用就類似一條責任鏈,一旦其尋找到責任者,那
么將由責任者持有并消費掉該次事件,具體體現在 View 的 onTouchEvent 方法
中返回值的設置,如果 onTouchEvent 返回 false,那么意味著當前 View 不會是
該次事件的責任人,將不會對其持有;如果為 true 則相反,此時 View 會持有該
事件并不再向下傳遞。
優點:
將請求者和處理者關系解耦,提供代碼的靈活性。
缺點:對鏈中請求處理者的遍歷中,如果處理者太多,那么遍歷必定會影響性能,特別
是在一些遞歸調用中,要慎重。
RxJava 的觀察者模式:
定義:定義對象間一種一對多的依賴關系,使得每當一個對象改變狀態,則所有
依賴于它的對象都會得到通知并被自動更新。
ListView/RecyclerView 的 Adapter 的 notifyDataSetChanged 方法、廣播、事件
總線機制。
觀察者模式主要的作用就是對象解耦,將觀察者與被觀察者完全隔離,只依賴于
Observer 和 Observable 抽象。
優點:
?
觀察者和被觀察者之間是抽象耦合,應對業務變化。
?
增強系統靈活性、可擴展性。
缺點:
?
在 Java 中消息的通知默認是順序執行,一個觀察者卡頓,會影響整體的
執行效率,在這種情況下,一般考慮采用異步的方式。
AIDL 代理模式:
定義:為其他對象提供一種代理以控制對這個對象的訪問。
靜態代理:代碼運行前代理類的 class 編譯文件就已經存在。
動態代理:通過反射動態地生成代理者的對象。代理誰將會在執行階段決定。將
原來代理類所做的工作由 InvocationHandler 來處理。
使用場景:
?
當無法或不想直接訪問某個對象或訪問某個對象存在困難時可以通過一
個代理對象來間接訪問,為了保證客戶端使用的透明性,委托對象與代理
對象需要實現相同的接口。
缺點:?
對類的增加。
ListView/RecyclerView/GridView 的適配器模式:
適配器模式把一個類的接口變換成客戶端所期待的另一種接口,從而使原本因接
口不匹配而無法在一起工作的兩個類能夠在一起工作。
使用場景:
?
接口不兼容。
?
想要建立一個可以重復使用的類。
?
需要一個統一的輸出接口,而輸入端的類型不可預知。
優點:
?
更好的復用性:復用現有的功能。
?
更好的擴展性:擴展現有的功能。
缺點:
?
過多地使用適配器,會讓系統非常零亂,不易于整體把握。例如,明明看
到調用的是 A 接口,其實內部被適配成了 B 接口的實現,一個系統如果
出現太多這種情況,無異于一場災難。
Context/ContextImpl 外觀模式:
要求一個子系統的外部與其內部的通信必須通過一個統一的對象進行,門面模式
提供一個高層次的接口,使得子系統更易于使用。
使用場景:
?
為一個復雜子系統提供一個簡單接口。
優點:
?
對客戶程序隱藏子系統細節,因而減少了客戶對于子系統的耦合,能夠擁
抱變化。
?
外觀類對子系統的接口封裝,使得系統更易用使用。
缺點:
?
外觀類接口膨脹。
?
外觀類沒有遵循開閉原則,當業務出現變更時,可能需要直接修改外觀類。二、集合框架 (???)
1、集合框架,list,map,set 都有哪些具體的實現類,區別都是什么?
Java 集合里使用接口來定義功能,是一套完善的繼承體系。Iterator 是所有集合
的總接口,其他所有接口都繼承于它,該接口定義了集合的遍歷操作,Collection
接口繼承于 Iterator,是集合的次級接口(Map 獨立存在),定義了集合的一些
通用操作。
Java 集合的類結構圖如下所示:
List:有序、可重復;索引查詢速度快;插入、刪除伴隨數據移動,速度慢;
Set:無序,不可重復;
Map:鍵值對,鍵唯一,值多個;
1.List,Set 都是繼承自 Collection 接口,Map 則不是;
2.List 特點:元素有放入順序,元素可重復;
Set 特點:元素無放入順序,元素不可重復,重復元素會蓋掉,(注意:元素雖
然無放入順序,但是元素在 set 中位置是由該元素的 HashCode 決定的,其位置
其實是固定,加入 Set 的 Object 必須定義 equals()方法;另外 list 支持 for 循環,也就是通過下標來遍歷,也可以使用迭代器,但是 set
只能用迭代,因為他無序,無法用下標取得想要的值)。
3.Set 和 List 對比:
Set:檢索元素效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改
變。
List:和數組類似,List 可以動態增長,查找元素效率高,插入刪除元素效率低,
因為會引起其他元素位置改變。
4.Map 適合儲存鍵值對的數據。
5.線程安全集合類與非線程安全集合類
LinkedList、ArrayList、HashSet 是非線程安全的,Vector 是線程安全的;
HashMap 是非線程安全的,HashTable 是線程安全的;
StringBuilder 是非線程安全的,StringBuffer 是線程安的。
下面是這些類具體的使用介紹:
ArrayList 與 LinkedList 的區別和適用場景
Arraylist:
優點:ArrayList 是實現了基于動態數組的數據結構,因地址連續,一旦數據存儲
好了,查詢操作效率會比較高(在內存里是連著放的)。
缺點:因為地址連續,ArrayList 要移動數據,所以插入和刪除操作效率比較低。
LinkedList:優點:LinkedList 基于鏈表的數據結構,地址是任意的,其在開辟內存空間的時
候不需要等一個連續的地址,對新增和刪除操作 add 和 remove,LinedList 比較
占優勢。LikedList 適用于要頭尾操作或插入指定位置的場景。
缺點:因為 LinkedList 要移動指針,所以查詢操作性能比較低。
適用場景分析:
當需要對數據進行對此訪問的情況下選用 ArrayList,當要對數據進行多次增加刪
除修改時采用 LinkedList。
ArrayList 和 LinkedList 怎么動態擴容的嗎?
ArrayList:
ArrayList 初始化大小是 10 (如果你知道你的 arrayList 會達到多少容量,可以
在初始化的時候就指定,能節省擴容的性能開支) 擴容點規則是,新增的時候
發現容量不夠用了,就去擴容 擴容大小規則是,擴容后的大小= 原始大小+原
始大小/2 + 1。(例如:原始大小是 10 ,擴容后的大小就是 10 + 5+1 = 16)
LinkedList:
linkedList 是一個雙向鏈表,沒有初始化大小,也沒有擴容的機制,就是一直在
前面或者后面新增就好。
ArrayList 與 Vector 的區別和適用場景
ArrayList 有三個構造方法:
public ArrayList(intinitialCapacity)// 構造一個具有指定初始容量的空列表。
public ArrayList()// 構造一個初始容量為 10 的空列表。public ArrayList(Collection<? extends E> c)// 構造一個包含指定 collection 的元
素的列表
Vector 有四個構造方法:
public Vector() // 使用指定的初始容量和等于零的容量增量構造一個空向量。
public Vector(int initialCapacity) // 構造一個空向量,使其內部數據數組的大小,其
標準容量增量為零。
public Vector(Collection<? extends E> c)// 構造一個包含指定 collection 中的元
素的向量
public Vector(int initialCapacity, int capacityIncrement)// 使用指定的初始容量
和容量增量構造一個空的向量
ArrayList 和 Vector 都是用數組實現的,主要有這么四個區別:
1)Vector 是多線程安全的,線程安全就是說多線程訪問代碼,不會產生不確定的
結果。而 ArrayList 不是,這可以從源碼中看出,Vector 類中的方法很多有
synchronied 進行修飾,這樣就導致了 Vector 在效率上無法與 ArrayLst 相比;
2)兩個都是采用的線性連續空間存儲元素,但是當空間充足的時候,兩個類的增
加方式是不同。
3)Vector 可以設置增長因子,而 ArrayList 不可以。
4)Vector 是一種老的動態數組,是線程同步的,效率很低,一般不贊成使用。
適用場景:
1.Vector 是線程同步的,所以它也是線程安全的,而 ArraList 是線程異步的,是
不安全的。如果不考慮到線程的安全因素,一般用 ArrayList 效率比較高。
2.如果集合中的元素的數目大于目前集合數組的長度時,在集合中使用數據量比
較大的數據,用 Vector 有一定的優勢。HashSet 與 TreeSet 的區別和適用場景
1.TreeSet 是二叉樹(紅黑樹的樹據結構)實現的,Treest 中的數據是自動排好
序的,不允許放入 null 值。
2.HashSet 是哈希表實現的,HashSet 中的數據是無序的可以放入 null,但只能
放入一個 null,兩者中的值都不重復,就如數據庫中唯一約束。
3.HashSet 要求放入的對象必須實現 HashCode()方法,放的對象,是以 hashcode
碼作為標識的,而具有相同內容的 String 對象,hashcode 是一樣,所以放入的
內容不能重復但是同一個類的對象可以放入不同的實例。
適用場景分析:
HashSet 是基于 Hash 算法實現的,其性能通常都優于 TreeSet。為快速查找而
設計的 Set,我們通常都應該使用 HashSet,在我們需要排序的功能時,我們才
使用 TreeSet。
HashMap 與 TreeMap、HashTable 的區別及適用場景
HashMap 非線程安全
HashMap:基于哈希表(散列表)實現。使用 HashMap 要求的鍵類明確定義了
hashCode()和 equals()[可以重寫 hasCode()和 equals()],為了優化 HashMap 空
間的使用,您可以調優初始容量和負載因子。其中散列表的沖突處理主分兩種,
一種是開放定址法,另一種是鏈表法。HashMap 實現中采用的是鏈表法。
TreeMap:非線程安全基于紅黑樹實現。TreeMap 沒有調優選項,因為該樹總處
于平衡狀態。
適用場景分析:HashMap 和 HashTable:HashMap 去掉了 HashTable 的 contain 方法,但是加上
了 containsValue()和 containsKey()方法。HashTable 是同步的,而 HashMap 是
非同步的,效率上比 HashTable 要高。HashMap 允許空鍵值,而 HashTable 不
允許。
HashMap:適用于 Map 中插入、刪除和定位元素。
Treemap:適用于按自然順序或自定義順序遍歷鍵(key)。 (ps:其實我們工作的過
程中對集合的使用是很頻繁的,稍注意并總結積累一下,在面試的時候應該會回答
的很輕松)
2、set 集合從原理上如何保證不重復?
1)在往 set 中添加元素時,如果指定元素不存在,則添加成功。
2)具體來講:當向 HashSet 中添加元素的時候,首先計算元素的 hashcode 值,
然后用這個(元素的 hashcode)%(HashMap 集合的大小)+1 計算出這個元
素的存儲位置,如果這個位置為空,就將元素添加進去;如果不為空,則用 equals
方法比較元素是否相等,相等就不添加,否則找一個空位添加。
3、HashMap 和 HashTable 的主要區別是什么?,兩者底層實現的數據結構是什么?
HashMap 和 HashTable 的區別:
二者都實現了 Map 接口,是將唯一的鍵映射到特定的值上,主要區別在于:
1)HashMap 沒有排序,允許一個 null 鍵和多個 null 值,而 Hashtable 不允許;
2)HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsvalue 和
containsKey, 因為 contains 方法容易讓人引起誤解;3)Hashtable 繼承自 Dictionary 類,HashMap 是 Java1.2 引進的 Map 接口的
實現;
4)Hashtable 的方法是 Synchronized 的,而 HashMap 不是,在多個線程訪問
Hashtable 時,不需要自己為它的方法實現同步,而 HashMap 就必須為之提供
額外的同步。Hashtable 和 HashMap 采用的 hash/rehash 算法大致一樣,所以
性能不會有很大的差異。
HashMap 和 HashTable 的底層實現數據結構:
HashMap 和 Hashtable 的底層實現都是數組 + 鏈表結構實現的(jdk8 以前)
4、HashMap、ConcurrentHashMap、hash()相關原理解析?
HashMap 1.7 的原理:
HashMap 底層是基于 數組 + 鏈表 組成的,不過在 jdk1.7 和 1.8 中具體實
現稍有不同。
負載因子:
?
給定的默認容量為 16,負載因子為 0.75。Map 在使用過程中不斷的往
里面存放數據,當數量達到了 16 * 0.75 = 12 就需要將當前 16 的容量
進行擴容,而擴容這個過程涉及到 rehash、復制數據等操作,所以非常
消耗性能。
?
因此通常建議能提前預估 HashMap 的大小最好,盡量的減少擴容帶來
的性能損耗。
其實真正存放數據的是 Entry<K,V>[] table,Entry 是 HashMap 中的一個靜態
內部類,它有 key、value、next、hash(key 的 hashcode)成員變量。
put 方法:?
判斷當前數組是否需要初始化。
?
如果 key 為空,則 put 一個空值進去。
?
根據 key 計算出 hashcode。
?
根據計算出的 hashcode 定位出所在桶。
?
如果桶是一個鏈表則需要遍歷判斷里面的 hashcode、key 是否和傳入
key 相等,如果相等則進行覆蓋,并返回原來的值。
?
如果桶是空的,說明當前位置沒有數據存入,新增一個 Entry 對象寫入
當前位置。(當調用 addEntry 寫入 Entry 時需要判斷是否需要擴容。
如果需要就進行兩倍擴充,并將當前的 key 重新 hash 并定位。而在
createEntry 中會將當前位置的桶傳入到新建的桶中,如果當前桶有值就
會在位置形成鏈表。)
get 方法:
?
首先也是根據 key 計算出 hashcode,然后定位到具體的桶中。
?
判斷該位置是否為鏈表。
?
不是鏈表就根據 key、key 的 hashcode 是否相等來返回值。
?
為鏈表則需要遍歷直到 key 及 hashcode 相等時候就返回值。
?
啥都沒取到就直接返回 null 。
HashMap 1.8 的原理:
當 Hash 沖突嚴重時,在桶上形成的鏈表會變的越來越長,這樣在查詢時的效
率就會越來越低;時間復雜度為 O(N),因此 1.8 中重點優化了這個查詢效率。
TREEIFY_THRESHOLD 用于判斷是否需要將鏈表轉換為紅黑樹的閾值。HashEntry 修改為 Node。
put 方法:
?
判斷當前桶是否為空,空的就需要初始化(在 resize 方法 中會判斷是否
進行初始化)。
?
根據當前 key 的 hashcode 定位到具體的桶中并判斷是否為空,為空表
明沒有 Hash 沖突就直接在當前位置創建一個新桶即可。
?
如果當前桶有值( Hash 沖突),那么就要比較當前桶中的 key、key 的
hashcode 與寫入的 key 是否相等,相等就賦值給 e,在第 8 步的時候會
統一進行賦值及返回。
?
如果當前桶為紅黑樹,那就要按照紅黑樹的方式寫入數據。
?
如果是個鏈表,就需要將當前的 key、value 封裝成一個新節點寫入到當
前桶的后面(形成鏈表)。
?
接著判斷當前鏈表的大小是否大于預設的閾值,大于時就要轉換為紅黑
樹。
?
如果在遍歷過程中找到 key 相同時直接退出遍歷。
?
如果 e != null 就相當于存在相同的 key,那就需要將值覆蓋。
?
最后判斷是否需要進行擴容。
get 方法:
?
首先將 key hash 之后取得所定位的桶。
?
如果桶為空則直接返回 null 。
?
否則判斷桶的第一個位置(有可能是鏈表、紅黑樹)的 key 是否為查詢的
key,是就直接返回 value。
?
如果第一個不匹配,則判斷它的下一個是紅黑樹還是鏈表。
?
紅黑樹就按照樹的查找方式返回值。?
不然就按照鏈表的方式遍歷匹配返回值。
修改為紅黑樹之后查詢效率直接提高到了 O(logn)。但是 HashMap 原有的問題
也都存在,比如在并發場景下使用時容易出現死循環:
?
在 HashMap 擴容的時候會調用 resize() 方法,就是這里的并發操作容
易在一個桶上形成環形鏈表;這樣當獲取一個不存在的 key 時,計算出
的 index 正好是環形鏈表的下標就會出現死循環:在 1.7 中 hash 沖突
采用的頭插法形成的鏈表,在并發條件下會形成循環鏈表,一旦有查詢落
到了這個鏈表上,當獲取不到值時就會死循環。
ConcurrentHashMap 1.7 原理:
ConcurrentHashMap 采用了分段鎖技術,其中 Segment 繼承于
ReentrantLock。不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同
步處理,理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數組數量)
的線程并發。每當一個線程占用鎖訪問一個 Segment 時,不會影響到其他的
Segment。
put 方法:
首先是通過 key 定位到 Segment,之后在對應的 Segment 中進行具體的
put。
雖然 HashEntry 中的 value 是用 volatile 關鍵詞修飾的,但是并不能保
證并發的原子性,所以 put 操作時仍然需要加鎖處理。
首先第一步的時候會嘗試獲取鎖,如果獲取失敗肯定就有其他線程存在競
爭,則利用 scanAndLockForPut() 自旋獲取鎖:嘗試自旋獲取鎖。 如果重試的次數達到了 MAX_SCAN_RETRIES 則改為
阻塞鎖獲取,保證能獲取成功。
將當前 Segment 中的 table 通過 key 的 hashcode 定位到
HashEntry。
遍歷該 HashEntry,如果不為空則判斷傳入的 key 和當前遍歷的 key 是
否相等,相等則覆蓋舊的 value。
為空則需要新建一個 HashEntry 并加入到 Segment 中,同時會先判斷
是否需要擴容。
最后會使用 unlock()解除當前 Segment 的鎖。
get 方法:
?
只需要將 Key 通過 Hash 之后定位到具體的 Segment ,再通過一次
Hash 定位到具體的元素上。
?
由于 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了內
存可見性,所以每次獲取時都是最新值。
? ConcurrentHashMap 的 get 方法是非常高效的,因為整個過程都不需要
加鎖。
ConcurrentHashMap 1.8 原理:
1.7 已經解決了并發問題,并且能支持 N 個 Segment 這么多次數的并發,但
依然存在 HashMap 在 1.7 版本中的問題:那就是查詢遍歷鏈表效率太低。和
1.8 HashMap 結構類似:其中拋棄了原有的 Segment 分段鎖,而采用了 CAS +
synchronized 來保證并發安全性。CAS:
如果 obj 內的 value 和 expect 相等,就證明沒有其他線程改變過這個變量,那
么就更新它為 update,如果這一步的 CAS 沒有成功,那就采用自旋的方式繼續
進行 CAS 操作。
問題:
?
目前在 JDK 的 atomic 包里提供了一個類 AtomicStampedReference 來解
決 ABA 問題。這個類的 compareAndSet 方法作用是首先檢查當前引用是
否等于預期引用,并且當前標志是否等于預期標志,如果全部相等,則以
原子方式將該引用和該標志的值設置為給定的更新值。
?
如果 CAS 不成功,則會原地自旋,如果長時間自旋會給 CPU 帶來非常大
的執行開銷。
put 方法:
?
根據 key 計算出 hashcode 。
?
判斷是否需要進行初始化。
?
如果當前 key 定位出的 Node 為空表示當前位置可以寫入數據,利用
CAS 嘗試寫入,失敗則自旋保證成功。
?
如果當前位置的 hashcode == MOVED == -1,則需要進行擴容。
?
如果都不滿足,則利用 synchronized 鎖寫入數據。
?
最后,如果數量大于 TREEIFY_THRESHOLD 則要轉換為紅黑樹。
get 方法:
?
根據計算出來的 hashcode 尋址,如果就在桶上那么直接返回值。?
如果是紅黑樹那就按照樹的方式獲取值。
?
就不滿足那就按照鏈表的方式遍歷獲取值。
1.8 在 1.7 的數據結構上做了大的改動,采用紅黑樹之后可以保證查詢效率
(O(logn)),甚至取消了 ReentrantLock 改為了 synchronized,這樣可以看出
在新版的 JDK 中對 synchronized 優化是很到位的。
HashMap、ConcurrentHashMap 1.7/1.8 實現原理
hash()算法全解析
HashMap 何時擴容:
當向容器添加元素的時候,會判斷當前容器的元素個數,如果大于等于閾值---
即大于當前數組的長度乘以加載因子的值的時候,就要自動擴容。
擴容的算法是什么:
擴容(resize)就是重新計算容量,向 HashMap 對象里不停的添加元素,而
HashMap 對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,
以便能裝入更多的元素。當然 Java 里的數組是無法自動擴容的,方法是使用一
個新的數組代替已有的容量小的數組。
Hashmap 如何解決散列碰撞(必問)?
Java 中 HashMap 是利用“拉鏈法”處理 HashCode 的碰撞問題。在調用 HashMap
的 put 方法或 get 方法時,都會首先調用 hashcode 方法,去查找相關的 key,
當有沖突時,再調用 equals 方法。hashMap 基于 hasing 原理,我們通過 put
和 get 方法存取對象。當我們將鍵值對傳遞給 put 方法時,他調用鍵對象的
hashCode()方法來計算 hashCode,然后找到 bucket(哈希桶)位置來存儲對象。當獲取對象時,通過鍵對象的 equals()方法找到正確的鍵值對,然后返回值對象。
HashMap 使用鏈表來解決碰撞問題,當碰撞發生了,對象將會存儲在鏈表的下
一個節點中。hashMap 在每個鏈表節點存儲鍵值對對象。當兩個不同的鍵卻有
相同的 hashCode 時,他們會存儲在同一個 bucket 位置的鏈表中。鍵對象的
equals()來找到鍵值對。
Hashmap 底層為什么是線程不安全的?
?
并發場景下使用時容易出現死循環,在 HashMap 擴容的時候會調用
resize() 方法,就是這里的并發操作容易在一個桶上形成環形鏈表;這樣
當獲取一個不存在的 key 時,計算出的 index 正好是環形鏈表的下標就
會出現死循環;
?
在 1.7 中 hash 沖突采用的頭插法形成的鏈表,在并發條件下會形成循
環鏈表,一旦有查詢落到了這個鏈表上,當獲取不到值時就會死循環。
5、ArrayMap 跟 SparseArray 在 HashMap 上面的改進?
HashMap 要存儲完這些數據將要不斷的擴容,而且在此過程中也需要不斷的做
hash 運算,這將對我們的內存空間造成很大消耗和浪費。
SparseArray:
SparseArray 比 HashMap 更省內存,在某些條件下性能更好,主要是因為它避
免了對 key 的自動裝箱(int 轉為 Integer 類型),它內部則是通過兩個數組來進
行數據存儲的,一個存儲 key,另外一個存儲 value,為了優化性能,它內部對
數據還采取了壓縮的方式來表示稀疏數組的數據,從而節約內存空間,我們從源
碼中可以看到 key 和 value 分別是用數組表示:
private int[] mKeys;private Object[] mValues;
同時,SparseArray 在存儲和讀取數據時候,使用的是二分查找法。也就是在 put
添加數據的時候,會使用二分查找法和之前的 key 比較當前我們添加的元素的
key 的大小,然后按照從小到大的順序排列好,所以,SparseArray 存儲的元素
都是按元素的 key 值從小到大排列好的。 而在獲取數據的時候,也是使用二分
查找法判斷元素的位置,所以,在獲取數據的時候非常快,比 HashMap 快的多。
ArrayMap:
ArrayMap 利用兩個數組,mHashes 用來保存每一個 key 的 hash 值,mArrray
大小為 mHashes 的 2 倍,依次保存 key 和 value。
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
當插入時,根據 key 的 hashcode()方法得到 hash 值,計算出在 mArrays 的 index
位置,然后利用二分查找找到對應的位置進行插入,當出現哈希沖突時,會在
index 的相鄰位置插入。
假設數據量都在千級以內的情況下:
1、如果 key 的類型已經確定為 int 類型,那么使用 SparseArray,因為它避免了
自動裝箱的過程,如果 key 為 long 類型,它還提供了一個 LongSparseArray 來
確保 key 為 long 類型時的使用
2、如果 key 類型為其它的類型,則使用 ArrayMap。
三、反射 (???)1、說說你對 Java 反射的理解?
答:Java 中的反射首先是能夠獲取到 Java 中要反射類的字節碼, 獲取字節碼
有三種方法:
1.Class.forName(className)
2.類名.class
3.this.getClass()。
然后將字節碼中的方法,變量,構造函數等映射成相應的 Method、Filed、
Constructor 等類,這些類提供了豐富的方法可以被我們所使用。
深入解析 Java 反射(1) - 基礎
Java 基礎之—反射(非常重要)
四、泛型 (??)
1、簡單介紹一下 java 中的泛型,泛型擦除以及相關的概念,解析與分派?
泛型是 Java SE1.5 的新特性,泛型的本質是參數化類型,也就是說所操的數據類
型被指定為一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱
為泛型類、泛型接口、泛型方法。 Java 語言引入泛型的好處是安全簡單。
在 Java SE 1.5 之前,沒有泛型的情況的下,通過對類型 Object 的引用來實現參
數的“任意化”,“任意化”帶來的缺點是要做顯式的強制類型轉換,而這種轉換是
要求開發者實際參數類型可以預知的情況下進行的。對于強制類型換錯誤的情
況,編譯器可能不提示錯誤,在運行的時候出現異常,這是一個安全隱患。
泛型的好處是在編譯的時候檢查類型安全,并且所有的轉換都是自動和隱式的,
提高代碼的重用率。1、泛型的類型參數只能是類類型(包括自定義類),不是簡單類型。
2、同一種泛型可以對應多個版本(因為參數類型是不確的),不同版本的泛型
類實例是不兼容的。
3、泛型的類型參數可以有多個。
4、泛型的參數類型可以使用 extends 語句,例如。習慣上稱為“有界類型”。
5、泛型的參數類型還可以是通配符類型。例如 Class<?> classType =
Class.forName("java.lang.String");
泛型擦除以及相關的概念
泛型信息只存在代碼編譯階段,在進入 JVM 之前,與泛型關的信息都會被擦除
掉。
在類型擦除的時候,如果泛型類里的類型參數沒有指定上限,則會被轉成 Object
類型,如果指定了上限,則會被傳轉換成對應的類型上限。
Java 中的泛型基本上都是在編譯器這個層次來實現的。生成的 Java 字節碼中是
不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編
譯的時候擦除掉。這個過程就稱為類型擦除。
類型擦除引起的問題及解決方法:
1、先檢查,在編譯,以及檢查編譯的對象和引用傳遞的題
2、自動類型轉換
3、類型擦除與多態的沖突和解決方法
4、泛型類型變量不能是基本數據類型5、運行時類型查詢
6、異常中使用泛型的問題
7、數組(這個不屬于類型擦除引起的問題)
9、類型擦除后的沖突
10、泛型在靜態方法和靜態類中的問題
五、注解 (??)
1、說說你對 Java 注解的理解?
注解相當于一種標記,在程序中加了注解就等于為程序打上了某種標記。程序可
以利用 ava 的反射機制來了解你的類及各種元素上有無何種標記,針對不同的標
記,就去做相應的事件。標記可以加在包,類,字段,方法,方法的參數以及局
部變量上。
六、其它 (??)
1、Java 的 char 是兩個字節,是怎么存 Utf-8 的字符的?
是否熟悉 Java char 和字符串(初級)
? char 是 2 個字節,utf-8 是 1~3 個字節。
?
字符集(字符集不是編碼):ASCII 碼與 Unicode 碼。
?
字符 -> 0xd83dde00(碼點)。
是否了解字符的映射和存儲細節(中級)
人類認知:字符 => 字符集:0x4e2d(char) => 計算機存儲(byte):01001110:4e、
00101101:2d編碼:UTF-16
“中”.getBytes("utf-6"); -> fe ff 4e 2d:4 個字節,其中前面的 fe ff 只是字節序標
志。
是否能觸類旁通,橫向對比其他語言(高級)
Python2 的字符串:
? byteString = "中"
? unicodeString = u"中"
令人迷惑的字符串長度
emoij = u"表情"
print(len(emoji)
Java 與 python 3.2 及以下:2 字節 python >= 3.3:1 字節
注意:Java 9 對 latin 字符的存儲空間做了優化,但字符串長度還是!= 字符數。
總結
? Java char 不存 UTF-8 的字節,而是 UTF-16。
? Unicode 通用字符集占兩個字節,例如“中”。
? Unicode 擴展字符集需要用一對 char 來表示,例如“表情”。
? Unicode 是字符集,不是編碼,作用類似于 ASCII 碼。
? Java String 的 length 不是字符數。
2、Java String 可以有多長?
是否對字符串編解碼有深入了解(中級)分配到棧:
String longString = "aaa...aaa";
分配到堆:
byte[] bytes = loadFromFile(new File("superLongText.txt");
String superLongString = new String(bytes);
是否對字符串在內存當中的存儲形式有深入了解(高級)
是否對 Java 虛擬機字節碼有足夠的了解(高級)
源文件:*.java
String longString = "aaa...aaa";
字節數 <= 65535
字節碼:*.class
CONSTANT_Utf8_info {
u1 tag;
u2 length;
(0~65535) u1 bytes[length];
最多 65535 個字節
}
javac 的編譯器有問題,< 65535 應該改為< = 65535。
Java String 棧分配
?
受字節碼限制,字符串最終的 MUTF-8 字節數不超過 65535。
? Latin 字符,受 Javac 代碼限制,最多 65534 個。
?
非 Latin 字符最終對應字節個數差異較大,最多字節個數是 65535。?
如果運行時方法區設置較小,也會受到方法區大小的限制。
是否對 java 虛擬機指令有一定的認識(高級)
new String(bytes)內部是采用了一個字符數組,其對應的虛擬機指令是 newarray
[int] ,數組理論最大個數為 Integer.MAX_VALUE,有些虛擬機需要一些頭部信
息,所以 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8。
Java String 堆分配
?
受虛擬機指令限制,字符數理論上限為 Integer.MAX_VALUE。
?
受虛擬機實現限制,實際上限可能會小于 Integer.MAX_VALUE。
?
如果堆內存較小,也會受到堆內存的限制。
總結
Java String 字面量形式
?
字節碼中 CONSTANT_Utf8_info 的限制
? Javac 源碼邏輯的限制
?
方法區大小的限制
Java String 運行時創建在堆上的形式
? Java 虛擬機指令 newarray 的限制
? Java 虛擬機堆內存大小的限制
3、Java 的匿名內部類有哪些限制?
考察匿名內部類的概念和用法(初級)
?
匿名內部類的名字:沒有人類認知意義上的名字?
只能繼承一個父類或實現一個接口
?
包名.OuterClass$1,表示定位的第一個匿名內部類。外部類加$N,N 是
匿名內部類的順序。
考察語言規范以及語言的橫向對比等(中級)
匿名內部類的繼承結構:Java 中的匿名內部類不可以繼承,只有內部類才可以有
實現繼承、實現接口的特性。而 Kotlin 是的匿名內部類是支持繼承的,如
val runnableFoo = object: Foo(),Runnable {
override fun run() {
}
}
作為考察內存泄漏的切入點(高級)
匿名內部類的構造方法(深入源碼字節碼探索語言本質的能力):
?
匿名內部類會默認持有外部類的引用,可能會導致內存泄漏。
?
由編譯器生成的。
其參數列表包括
?
外部對象(定義在非靜態域內)
?
父類的外部對象(父類非靜態)
?
父類的構造方法參數(父類有構造方法且參數列表不為空)
?
外部捕獲的變量(方法體內有引用外部 final 變量)
Lambda 轉換(SAM 類型,僅支持單一接口類型):
如果 CallBack 是一個 interface,不是抽象類,則可以轉換為 Lambda 表達式。
CallBack callBack = () -> {
...};
總結
?
沒有人類認知意義上的名字。
?
只能繼承一個父類或實現一個接口。
?
父類是非靜態的類型,則需父類外部實例來初始化。
?
如果定義在非靜態作用域內,會引用外部類實例。
?
只能捕獲外部作用域內的 final 變量。
?
創建時只有單一方法的接口可以用 Lambda 轉換。
技巧點撥
關注語言版本的變化:
?
體現對技術的熱情
?
體現好學的品質
?
顯得專業
4、Java 中對異常是如何進行分類的?
異常整體分類:
Java 異常結構中定義有 Throwable 類。 Exception 和 Error 為其子類。
Error 是程序無法處理的錯誤,比如 OutOfMemoryError、StackOverflowError。
這些異常發生時, Java 虛擬機(JVM)一般會選擇線程終止。
Exception 是程序本身可以處理的異常,這種異常分兩大類運行時異常和非運行
時異常,程序中應當盡可能去處理這些異常。
運行時異常都是 RuntimeException 類及其子類異常,如 NullPointerException、
IndexOutOfBoundsException 等, 這些異常是不檢查異常,程序中可以選擇捕
獲處理,也可以不處理。這些異常一般是由程序邏輯錯誤引起的, 程序應該從
邏輯角度盡可能避免這類異常的發生。異常處理的兩個基本原則:
1、盡量不要捕獲類似 Exception 這樣的通用異常,而是應該捕獲特定異常。
2、不要生吞異常。
NoClassDefFoundError 和 ClassNotFoundException 有什么區別?
ClassNotFoundException 的產生原因主要是: Java 支持使用反射方式在運行時
動態加載類,例如使用 Class.forName 方法來動態地加載類時,可以將類名作為
參數傳遞給上述方法從而將指定類加載到 JVM 內存中,如果這個類在類路徑中
沒有被找到,那么此時就會在運行時拋出 ClassNotFoundException 異常。 解決
該問題需要確保所需的類連同它依賴的包存在于類路徑中,常見問題在于類名書
寫錯誤。 另外還有一個導致 ClassNotFoundException 的原因就是:當一個類已
經某個類加載器加載到內存中了,此時另一個類加載器又嘗試著動態地從同一個
包中加載這個類。通過控制動態類加載過程,可以避免上述情況發生。
NoClassDefFoundError 產生的原因在于: 如果 JVM 或者 ClassLoader 實例嘗試
加載(可以通過正常的方法調用,也可能是使用 new 來創建新的對象)類的時
候卻找不到類的定義。要查找的類在編譯的時候是存在的,運行的時候卻找不到
了。這個時候就會導致 NoClassDefFoundError. 造成該問題的原因可能是打包過
程漏掉了部分類,或者 jar 包出現損壞或者篡改。解決這個問題的辦法是查找那
些在開發期間存在于類路徑下但在運行期間卻不在類路徑下的類。
5、String 為什么要設計成不可變的?
String 是不可變的(修改 String 時,不會在原有的內存地址修改,而是重新指向
一個新對象),String 用 final 修飾,不可繼承,String 本質上是個 final 的 char[]
數組,所以 char[]數組的內存地址不會被修改,而且 String 也沒有對外暴露修改
char[]數組的方法。不可變性可以保證線程安全以及字符串串常量池的實現。6、Java 里的冪等性了解嗎?
冪等性原本是數學上的一個概念,即:f(x) = f(f(x)),對同一個系統,使用同樣的
條件,一次請求和重復的多次請求對系統資源的影響是一致的。
冪等性最為常見的應用就是電商的客戶付款,試想一下如果你在付款的時候因為
網絡等各種問題失敗了,然后去重復的付了一次,是一種多么糟糕的體驗。冪等
性就是為了解決這樣的問題。
實現冪等性可以使用 Token 機制。
核心思想是為每一次操作生成一個唯一性的憑證,也就是 token。一個 token 在
操作的每一個階段只有一次執行權,一旦執行成功則保存執行結果。對重復的請
求,返回同一個結果。
例如:電商平臺上的訂單 id 就是最適合的 token。當用戶下單時,會經歷多個環
節,比如生成訂單,減庫存,減優惠券等等。每一個環節執行時都先檢測一下該
訂單 id 是否已經執行過這一步驟,對未執行的請求,執行操作并緩存結果,而
對已經執行過的 id,則直接返回之前的執行結果,不做任何操 作。這樣可以在
最大程度上避免操作的重復執行問題,緩存起來的執行結果也能用于事務的控制
等。
7、為什么 Java 里的匿名內部類只能訪問 final 修飾的外部變量?
匿名內部類用法:
public class TryUsingAnonymousClass {
public void useMyInterface() {
final Integer number = 123;
System.out.println(number);
MyInterface myInterface = new MyInterface() {
@Override
public void doSomething() {System.out.println(number);
}
};
myInterface.doSomething();
System.out.println(number);
}
}
編譯后的結果
class TryUsingAnonymousClass$1
implements MyInterface {
private final TryUsingAnonymousClass this$0;
private final Integer paramInteger;
TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer
paramInteger) {
this.this$0 = this$0;
this.paramInteger = paramInteger;
}
public void doSomething() {
System.out.println(this.paramInteger);
}
}
因為匿名內部類最終會編譯成一個單獨的類,而被該類使用的變量會以構造函數
參數的形式傳遞給該類,例如:Integer paramInteger,如果變量不定義成 final的,paramInteger 在匿名內部類被可以被修改,進而造成和外部的 paramInteger
不一致的問題,為了避免這種不一致的情況,因次 Java 規定匿名內部類只能訪
問 final 修飾的外部變量。
8、講一下 Java 的編碼方式?
為什么需要編碼
計算機存儲信息的最小單元是一個字節即 8bit,所以能示的范圍是 0~255,這個
范圍無法保存所有的字符,所以要一個新的數據結構 char 來表示這些字符,從
char 到 byte 需要編碼。
常見的編碼方式有以下幾種:
ASCII:總共有 128 個,用一個字節的低 7 位表示,031 是控制字符如換行回
車刪除等;32126 是打印字符,可以通過鍵盤輸入并且能夠顯示出來。
GBK:碼范圍是 8140~FEFE(去掉 XX7F)總共有 23940 個碼位,它能表示
21003 個漢字,它的編碼是和 GB2312 兼容的,也就是說用 GB2312 編碼的漢
字可以用 GBK 來解碼,并且不會有亂碼。
UTF-16:UTF-16 具體定義了 Unicode 字符在計算機中存取方法。UTF-16 用
兩個字節來表示 Unicode 轉化格式,這個是定長的表示方法,不論什么字符都
可以用兩個字節表示,兩個字節是 16 個 bit,所以叫 UTF-16。UTF-16 表示字
符非常方便,每兩個字節表示一個字符,這個在字符串操作時就大大簡化了操作,
這也是 Java 以 UTF-16 作為內存的字符存儲格式的一個很重要的原因。
UTF-8:統一采用兩個字節表示一個字符,雖然在表示上非常簡單方便,但是也
有其缺點,有很大一部分字符用一個字節就可以表示的現在要兩個字節表示,存
儲空間放大了一倍,在現在的網絡帶寬還非常有限的今天,這樣會增大網絡傳輸的流量,而且也沒必要。而 UTF-8 采用了一種變長技術,每個編碼區域有不同
的字碼長度。不同類型的字符可以是由 1~6 個字節組成。
Java 中需要編碼的地方一般都在字符到字節的轉換上,這個一般包括磁盤 IO 和
網絡 IO。
Reader 類是 Java 的 I/O 中讀字符的父類,而 InputStream 類是讀字節的父
類,InputStreamReader 類就是關聯字節到字符的橋梁,它負責在 I/O 過程中
處理讀取字節到字符的轉換,而具體字節到字符解碼實現由 StreamDecoder 去
實現,在 StreamDecoder 解碼過程中必須由用戶指定 Charset 編碼格式。
9、String,StringBuffer,StringBuilder 有哪些不同?
三者在執行速度方面的比較:StringBuilder > StringBuffer > String
String 每次變化一個值就會開辟一個新的內存空間
StringBuilder:線程非安全的
StringBuffer:線程安全的
對于三者使用的總結:
1.如果要操作少量的數據用 String。
2.單線程操作字符串緩沖區下操作大量數據用 StringBuilder。
3.多線程操作字符串緩沖區下操作大量數據用 StringBuffer。String 是 Java 語言非常基礎和重要的類,提供了構造和管理字符串的各種基本
邏輯。它是典型的 Immutable 類,被聲明成為 final class,所有屬性也都是 final
的。也由于它的不可變性,類似拼接、裁剪字符串等動作,都會產生新的 String
對象。由于字符串操作的普遍性,所以相關操作的效率往往對應用性能有明顯影
響。
StringBuffer 是為解決上面提到拼接產生太多中間對象的問題而提供的一個類,
我們可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定
位置。StringBuffer 本質是一個線程安全的可修改字符序列,它保證了線程安全,
也隨之帶來了額外的性能開銷,所以除非有線程安全的需要,不然還是推薦使用
它的后繼者,也就是 StringBuilder。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質區別,
但是它去掉了線程安全的部分,有效減小了開銷,是絕大部分情況下進行字符串
拼接的首選。
10、什么是內部類?內部類的作用。
內部類可以有多個實例,每個實例都有自己的狀態信息,并且與其他外圍對象的
信息相互獨立。
在單個外圍類中,可以讓多個內部類以不同的方式實現同一個接口,或者繼承同
一個類。
創建內部類對象并不依賴于外圍類對象的創建。
內部類并沒有令人迷惑的“is-a”關系,他就是一個獨立的實體。
內部類提供了更好的封裝,除了該外圍類,其他類都不能訪問。。
11、抽象類和接口區別?
共同點
?
是上層的抽象層。
?
都不能被實例化。?
都能包含抽象的方法,這些抽象的方法用于描述類具備的功能,但是不提
供具體的實現。
區別:
? 1、在抽象類中可以寫非抽象的方法,從而避免在子類中重復書寫他們,
這樣可以提高代碼的復用性,這是抽象類的優勢,接口中只能有抽象的方
法。
? 2、多繼承:一個類只能繼承一個直接父類,這個父類可以是具體的類也
可是抽象類,但是一個類可以實現多個接口。
? 3、抽象類可以有默認的方法實現,接口根本不存在方法的實現。
? 4、子類使用 extends 關鍵字來繼承抽象類。如果子類不是抽象類的話,
它需要提供抽象類中所有聲明方法的實現。子類使用關鍵字 implements
來實現接口。它需要提供接口中所有聲明方法的實現。
? 5、構造器:抽象類可以有構造器,接口不能有構造器。
? 6、和普通 Java 類的區別:除了你不能實例化抽象類之外,抽象類和普通
Java 類沒有任何區別,接口是完全不同的類型。
? 7、訪問修飾符:抽象方法可以有 public、protected 和 default 修飾符,接
口方法默認修飾符是 public。你不可以使用其它修飾符。
? 8、main 方法:抽象方法可以有 main 方法并且我們可以運行它接口沒有
main 方法,因此我們不能運行它。
? 9、速度:抽象類比接口速度要快,接口是稍微有點慢的,因為它需要時間
去尋找在類中實現的方法。
? 10、添加新方法:如果你往抽象類中添加新的方法,你可以給它提供默認
的實現。因此你不需要改變你現在的代碼。如果你往接口中添加方法,那
么你必須改變實現該接口的類。
12、接口的意義?規范、擴展、回調。
13、父類的靜態方法能否被子類重寫?
不能。子類繼承父類后,用相同的靜態方法和非靜態方法,這時非靜態方法覆蓋
父類中的方法(即方法重寫),父類的該靜態方法被隱藏(如果對象是父類則調
用該隱藏的方法),另外子類可繼承父類的靜態與非靜態方法,至于方法重載我
覺得它其中一要素就是在同一類中,不能說父類中的什么方法與子類里的什么方
法是方法重載的體現。
14、抽象類的意義?
為其子類提供一個公共的類型,封裝子類中的重復內容,定義抽象方法,子類雖
然有不同的實現 但是定義是一致的。
15、靜態內部類、非靜態內部類的理解?
靜態內部類:只是為了降低包的深度,方便類的使用,靜態內部類適用于包含在
類當中,但又不依賴與外在的類,不用使用外在類的非靜態屬性和方法,只是為
了方便管理類結構而定義。在創建靜態內部類的時候,不需要外部類對象的引用。
非靜態內部類:持有外部類的引用,可以自由使用外部類的所有變量和方法。
16、為什么復寫 equals 方法的同時需要復寫 hashcode 方法,前者相同后者是否相同,反過來
呢?為什么?
要考慮到類似 HashMap、HashTable、HashSet 的這種散列的數據類型的運用,
當我們重寫 equals 時,是為了用自身的方式去判斷兩個自定義對象是否相等,
然而如果此時剛好需要我們用自定義的對象去充當 hashmap 的鍵值使用時,就
會出現我們認為的同一對象,卻因為 hash 值不同而導致 hashmap 中存了兩個對
象,從而才需要進行 hashcode 方法的覆蓋。
17、equals 和 hashcode 的關系?
hashcode 和 equals 的約定關系如下:
1、如果兩個對象相等,那么他們一定有相同的哈希值(hashcode)。2、如果兩個對象的哈希值相等,那么這兩個對象有可能相等也有可能不
相等。(需要再通過 equals 來判斷)
18、java 為什么跨平臺?
因為 Java 程序編譯之后的代碼不是能被硬件系統直接運行的代碼,而是一種“中
間碼”——字節碼。然后不同的硬件平臺上安裝有不同的 Java 虛擬機(JVM),由
JVM 來把字節碼再“翻譯”成所對應的硬件平臺能夠執行的代碼。因此對于 Java
編程者來說,不需要考慮硬件平臺是什么。所以 Java 可以跨平臺。
19、浮點數的精準計算
BigDecimal 類進行商業計算,Float 和 Double 只能用來做科學計算或者是工程
計算。
20、final,finally,finalize 的區別?
final 可以用來修飾類、方法、變量,分別有不同的意義,final 修飾的 class 代
表不可以繼承擴展,final 的變量是不可以修改的,而 final 的方法也是不可以
重寫的(override)。
finally 則是 Java 保證重點代碼一定要被執行的一種機制。我們可以使用
try-finally 或者 try-catch-finally 來進行類似關閉 JDBC 連接、保證 unlock 鎖
等動作。
finalize 是基礎類 java.lang.Object 的一個方法,它的設計目的是保證對象在被
垃圾收集前完成特定資源的回收。finalize 機制現在已經不推薦使用,并且在
JDK 9 開始被標記為 deprecated。Java 平臺目前在逐步使用
java.lang.ref.Cleaner 來替換掉原有的 finalize 實現。Cleaner 的實現利用了幻象引用(PhantomReference),這是一種常見的所謂 post-mortem 清理機制。
利用幻象引用和引用隊列,我們可以保證對象被徹底銷毀前做一些類似資源回收
的工作,比如關閉文件描述符(操作系統有限的資源),它比 finalize 更加輕量、
更加可靠。
21、靜態內部類的設計意圖
靜態內部類與非靜態內部類之間存在一個最大的區別:非靜態內部類在編譯完成
之后會隱含地保存著一個引用,該引用是指向創建它的外圍內,但是靜態內部類
卻沒有。
沒有這個引用就意味著:
它的創建是不需要依賴于外圍類的。 它不能使用任何外圍類的非 static 成員變
量和方法。
22、Java 中對象的生命周期
在 Java 中,對象的生命周期包括以下幾個階段:
1.創建階段(Created)
JVM 加載類的 class 文件 此時所有的 static 變量和 static 代碼塊將被執行 加載
完成后,對局部變量進行賦值(先父后子的順序) 再執行 new 方法 調用構造
函數 一旦對象被創建,并被分派給某些變量賦值,這個對象的狀態就切換到了
應用階段。
2.應用階段(In Use)
對象至少被一個強引用持有著。
3.不可見階段(Invisible)
當一個對象處于不可見階段時,說明程序本身不再持有該對象的任何強引用,雖
然該這些引用仍然是存在著的。 簡單說就是程序的執行已經超出了該對象的作
用域了。4.不可達階段(Unreachable)
對象處于不可達階段是指該對象不再被任何強引用所持有。 與“不可見階段”相
比,“不可見階段”是指程序不再持有該對象的任何強引用,這種情況下,該對象
仍可能被 JVM 等系統下的某些已裝載的靜態變量或線程或 JNI 等強引用持有著,
這些特殊的強引用被稱為”GC root”。存在著這些 GC root 會導致對象的內存泄
露情況,無法被回收。
5.收集階段(Collected)
當垃圾回收器發現該對象已經處于“不可達階段”并且垃圾回收器已經對該對象
的內存空間重新分配做好準備時,則對象進入了“收集階段”。如果該對象已經重
寫了 finalize()方法,則會去執行該方法的終端操作。
6.終結階段(Finalized)
當對象執行完 finalize()方法后仍然處于不可達狀態時,則該對象進入終結階段。
在該階段是等待垃圾回收器對該對象空間進行回收。
7.對象空間重分配階段(De-allocated)
垃圾回收器對該對象的所占用的內存空間進行回收或者再分配了,則該對象徹底
消失了,稱之為“對象空間重新分配階段。
23、靜態屬性和靜態方法是否可以被繼承?是否可以被重寫?以及原因?
結論:java 中靜態屬性和靜態方法可以被繼承,但是不可以被重寫而是被隱藏。
原因:
1). 靜態方法和屬性是屬于類的,調用的時候直接通過類名.方法名完成,不需要
繼承機制即可以調用。如果子類里面定義了靜態方法和屬性,那么這時候父類的靜態方法或屬性稱之為"隱藏"。如果你想要調用父類的靜態方法和屬性,直接通
過父類名.方法或變量名完成,至于是否繼承一說,子類是有繼承靜態方法和屬
性,但是跟實例方法和屬性不太一樣,存在"隱藏"的這種情況。
2). 多態之所以能夠實現依賴于繼承、接口和重寫、重載(繼承和重寫最為關鍵)。
有了繼承和重寫就可以實現父類的引用指向不同子類的對象。重寫的功能是:"
重寫"后子類的優先級要高于父類的優先級,但是“隱藏”是沒有這個優先級之分
的。
3). 靜態屬性、靜態方法和非靜態的屬性都可以被繼承和隱藏而不能被重寫,因
此不能實現多態,不能實現父類的引用可以指向不同子類的對象。非靜態方法可
以被繼承和重寫,因此可以實現多態。
24、object 類的 equal 和 hashcode 方法重寫,為什么?
在 Java API 文檔中關于 hashCode 方法有以下幾點規定(原文來自 java 深入解
析一書):
1、在 java 應用程序執行期間,如果在 equals 方法比較中所用的信息沒有被修
改,那么在同一個對象上多次調用 hashCode 方法時必須一致地返回相同的整
數。如果多次執行同一個應用時,不要求該整數必須相同。
2、如果兩個對象通過調用 equals 方法是相等的,那么這兩個對象調用 hashCode
方法必須返回相同的整數。
3、如果兩個對象通過調用 equals 方法是不相等的,不要求這兩個對象調用
hashCode 方法必須返回不同的整數。但是程序員應該意識到對不同的對象產生
不同的 hash 值可以提供哈希表的性能。
25、java 中==和 equals 和 hashCode 的區別?默認情況下也就是從超類 Object 繼承而來的 equals 方法與‘==’是完全等價的,
比較的都是對象的內存地址,但我們可以重寫 equals 方法,使其按照我們的需
求的方式進行比較,如 String 類重寫了 equals 方法,使其比較的是字符的序列,
而不再是內存地址。在 java 的集合中,判斷兩個對象是否相等的規則是:
1.判斷兩個對象的 hashCode 是否相等。
2.判斷兩個對象用 equals 運算是否相等。
26、Java 的四種引用及使用場景?
?
強引用(FinalReference):在內存不足時不會被回收。平常用的最多的
對象,如新創建的對象。
?
軟引用(SoftReference):在內存不足時會被回收。用于實現內存敏感的
高速緩存。
?
弱引用(WeakReferenc):只要 GC 回收器發現了它,就會將之回收。用
于 Map 數據結構中,引用占用內存空間較大的對象。
?
虛引用(PhantomReference):在回收之前,會被放入 ReferenceQueue,
JVM 不會自動將該 referent 字段值設置成 null。其它引用被 JVM 回收之
后才會被放入 ReferenceQueue 中。用于實現一個對象被回收之前做一些
清理工作。
27、類的加載過程,Person person = new Person();為例進行說明。
1).因為 new 用到了 Person.class,所以會先找到 Person.class 文件,并加載到內
存中;
2).執行該類中的 static 代碼塊,如果有的話,給 Person.class 類進行初始化;
3).在堆內存中開辟空間分配內存地址;4).在堆內存中建立對象的特有屬性,并進行默認初始化;
5).對屬性進行顯示初始化;
6).對對象進行構造代碼塊初始化;
7).對對象進行與之對應的構造函數進行初始化;
8).將內存地址付給棧內存中的 p 變量。
28、JAVA 常量池
Interger 中的 128(-128~127)
a.當數值范圍為-128~127 時:如果兩個 new 出來的 Integer 對象,即使值相同,
通過“==”比較結果為 false,但兩個對直接賦值,則通過“==”比較結果為“true,
這一點與 String 非常相似。
b.當數值不在-128~127 時,無論通過哪種方式,即使兩對象的值相等,通過“==”
比較,其結果為 false;
c.當一個 Integer 對象直接與一個 int 基本數據類型通過“==”比較,其結果與第一
點相同;
d.Integer 對象的 hash 值為數值本身;
為什么是-128-127?
在 Integer 類中有一個靜態內部類 IntegerCache,在 IntegrCache 類中有一個
Integer 數組,用以緩存當前數值范圍為-128~127 時的 Integer 對象。29、在重寫 equals 方法時,需要遵循哪些約定,具體介紹一下?
重寫 equals 方法時需要遵循通用約定:自反性、對稱性、傳遞性、一致性、非
空性
1)自反性
對于任何非 null 的引用值 x,x.equals(x)必須返回 true。---這一點基本上不會有啥
問題
2)對稱性
對于任何非 null 的引用值 x 和 y,當且僅當 x.equals(y)為 true 時,y.equals(x)也
為 true。
3)傳遞性
對于任何非 null 的引用值 x、y、z。如果 x.equals(y)==true,y.equals(z)==true,
那么 x.equals(z)==true。
4) 一致性
對于任何非 null 的引用值 x 和 y,只要 equals 的比較操作在對象所用的信息沒
有被修改,那么多次調用 x.equals(y)就會一致性地返回 true,或者一致性的返回
false。
5)非空性
所有比較的對象都不能為空。
30、深拷貝和淺拷貝的區別
31、Integer 類對 int 的優化第二節 Java 并發面試題
一、線程池相關 (???)
1、什么是線程池,如何使用?為什么要使用線程池?
答:線程池就是事先將多個線程對象放到一個容器中,使用的時候就不用 new
線程而是直接去池中拿線程即可,節 省了開辟子線程的時間,提高了代碼執行
效率。
2、Java 中的線程池共有幾種?
Java 有四種線程池:
第一種:newCachedThreadPool
不固定線程數量,且支持最大為 Integer.MAX_VALUE 的線程數量:
public static ExecutorService newCachedThreadPool() {
// 這個線程池 corePoolSize 為 0,maximumPoolSize 為 Integer.MAX_VALUE
// 意思也就是說來一個任務就創建一個 woker,回收時間是 60s
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可緩存線程池:
1、線程數無限制。 2、有空閑線程則復用空閑線程,若無空閑線程則新建線程。
3、一定程序減少頻繁創建/銷毀線程,減少系統開銷。
第二種:newFixedThreadPool一個固定線程數量的線程池:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory
threadFactory) {
// corePoolSize 跟 maximumPoolSize 值一樣,同時傳入一個無界阻塞隊列
// 該線程池的線程會維持在指定線程數,不會進行回收
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
定長線程池:
1、可控制線程最大并發數(同時執行的線程數)。 2、超出的線程會在隊列中
等待。
第三種:newSingleThreadExecutor
可以理解為線程數量為 1 的 FixedThreadPool:
public static ExecutorService newSingleThreadExecutor() {
// 線程池中只有一個線程進行任務執行,其他的都放入阻塞隊列
// 外面包裝的 FinalizableDelegatedExecutorService 類實現了 finalize 方法,在
JVM 垃圾回收的時候會關閉線程池
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}單線程化的線程池:
1、有且僅有一個工作線程執行任務。 2、所有任務按照指定順序執行,即遵循
隊列的入隊出隊規則。
第四種:newScheduledThreadPool。
支持定時以指定周期循環執行任務:
public static ScheduledExecutorService newScheduledThreadPool(int
corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
注意:前三種線程池是 ThreadPoolExecutor 不同配置的實例,最后一種是
ScheduledThreadPoolExecutor 的實例。
3、線程池原理?
從數據結構的角度來看,線程池主要使用了阻塞隊列(BlockingQueue)和
HashSet 集合構成。 從任務提交的流程角度來看,對于使用線程池的外部來說,
線程池的機制是這樣的:
1、如果正在運行的線程數 < coreSize,馬上創建核心線程執行該 task,不排隊等待;
2、如果正在運行的線程數 >= coreSize,把該 task 放入阻塞隊列;
3、如果隊列已滿 && 正在運行的線程數 < maximumPoolSize,創建新的非核心線程執行該
task;
4、如果隊列已滿 && 正在運行的線程數 >= maximumPoolSize,線程池調用 handler 的 reject
方法拒絕本次提交。
理解記憶:1-2-3-4 對應(核心線程->阻塞隊列->非核心線程->handler 拒絕提
交)。
線程池的線程復用:這里就需要深入到源碼 addWorker():它是創建新線程的關鍵,也是線程復用的
關鍵入口。最終會執行到 runWoker,它取任務有兩個方式:
? firstTask:這是指定的第一個 runnable 可執行任務,它會在 Woker 這個
工作線程中運行執行任務 run。并且置空表示這個任務已經被執行。
? getTask():這首先是一個死循環過程,工作線程循環直到能夠取出
Runnable 對象或超時返回,這里的取的目標就是任務隊列 workQueue,
對應剛才入隊的操作,有入有出。
其實就是任務在并不只執行創建時指定的 firstTask 第一任務,還會從任務隊列
的中通過 getTask()方法自己主動去取任務執行,而且是有/無時間限定的阻塞等
待,保證線程的存活。
信號量
semaphore 可用于進程間同步也可用于同一個進程間的線程同步。
可以用來保證兩個或多個關鍵代碼段不被并發調用。在進入一個關鍵代碼段之
前,線程必須獲取一個信號量;一旦該關鍵代碼段完成了,那么該線程必須釋放
信號量。其它想進入該關鍵代碼段的線程必須等待直到第一個線程釋放信號量。
4、線程池都有哪幾種工作隊列?
1、ArrayBlockingQueue
是一個基于數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素
進行排序。
2、LinkedBlockingQueue
一個基于鏈表結構的阻塞隊列,此隊列按 FIFO (先進先出) 排序元素,吞吐
量通常要高于 ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()和 Executors.newSingleThreadExecutor 使用了
這個隊列。
3、SynchronousQueue
一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,
否則插入操作一直處于阻塞狀態,吞吐量通常要高于 LinkedBlockingQueue,靜
態工廠方法 Executors.newCachedThreadPool 使用了這個隊列。
4、PriorityBlockingQueue
一個具有優先級的無限阻塞隊列。
5、怎么理解無界隊列和有界隊列?
有界隊列
1.初始的 poolSize < corePoolSize,提交的 runnable 任務,會直接做為 new 一
個 Thread 的參數,立馬執行 。 2.當提交的任務數超過了 corePoolSize,會將
當前的 runable 提交到一個 block queue 中。3.有界隊列滿了之后,如果 poolSize
< maximumPoolsize 時,會嘗試 new 一個 Thread 的進行救急處理,立馬執行
對應的 runnable 任務。 4.如果 3 中也無法處理了,就會走到第四步執行 reject
操作。
無界隊列
與有界隊列相比,除非系統資源耗盡,否則無界的任務隊列不存在任務入隊失敗
的情況。當有新的任務到來,系統的線程數小于 corePoolSize 時,則新建線程
執行任務。當達到 corePoolSize 后,就不會繼續增加,若后續仍有新的任務加
入,而沒有空閑的線程資源,則任務直接進入隊列等待。若任務創建和處理的速
度差異很大,無界隊列會保持快速增長,直到耗盡系統內存。 當線程池的任務緩存隊列已滿并且線程池中的線程數目達到 maximumPoolSize,如果還有任務
到來就會采取任務拒絕策略。
6、多線程中的安全隊列一般通過什么實現?
Java 提供的線程安全的 Queue 可以分為阻塞隊列和非阻塞隊列,其中阻塞隊列
的典型例子是 BlockingQueue,非阻塞隊列的典型例子是
ConcurrentLinkedQueue.
對于 BlockingQueue,想要實現阻塞功能,需要調用 put(e) take() 方法。而
ConcurrentLinkedQueue 是基于鏈接節點的、無界的、線程安全的非阻塞隊列。
二、Synchronized、volatile、Lock(ReentrantLock)相關 (???)
1、synchronized 的原理?
synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現的,
Monitor 對象是同步的基本實現,而 synchronized 同步方法使用了
ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執行
相應的同步調用。
在 Java 6 之前,Monitor 的實現完全是依靠操作系統內部的互斥鎖,因為需要
進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。
現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不同
的 Monitor 實現,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、
輕量級鎖和重量級鎖,大大改進了其性能。
所謂鎖的升級、降級,就是 JVM 優化 synchronized 運行的機制,當 JVM 檢
測到不同的競爭狀況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、
降級。當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操作,在對象頭上
的 Mark Word 部分設置線程 ID,以表示這個對象偏向于當前線程,所以并不
涉及真正的互斥鎖。這樣做的假設是基于在很多應用場景中,大部分對象生命周
期中最多會被一個線程鎖定,使用偏斜鎖可以降低無競爭開銷。
如果有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就需要撤銷
(revoke)偏斜鎖,并切換到輕量級鎖實現。輕量級鎖依賴 CAS 操作 Mark
Word 來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖;否則,進一步升
級為重量級鎖(可能會先進行自旋鎖升級,如果失敗再嘗試重量級鎖升級)。
我注意到有的觀點認為 Java 不會進行鎖降級。實際上據我所知,鎖降級確實是
會發生的,當 JVM 進入安全點(SafePoint)的時候,會檢查是否有閑置的
Monitor,然后試圖進行降級。
2、Synchronized 優化后的鎖機制簡單介紹一下,包括自旋鎖、偏向鎖、輕量級鎖、重量級鎖?
自旋鎖:
線程自旋說白了就是讓 cpu 在做無用功,比如:可以執行幾次 for 循環,可以執
行幾條空的匯編指令,目的是占著 CPU 不放,等待獲取鎖的機會。如果旋的時
間過長會影響整體性能,時間過短又達不到延遲阻塞的目的。
偏向鎖
偏向鎖就是一旦線程第一次獲得了監視對象,之后讓監視對象“偏向”這個線程,
之后的多次調用則可以避免 CAS 操作,說白了就是置個變量,如果發現為 true
則無需再走各種加鎖/解鎖流程。
輕量級鎖:
輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當
第二個線程加入鎖競爭用的時候,偏向鎖就會升級為輕量級鎖;重量級鎖
重量鎖在 JVM 中又叫對象監視器(Monitor),它很像 C 中的 Mutex,除了具備
Mutex(0|1)互斥的功能,它還負責實現了 Semaphore(信號量)的功能,也就是說
它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait 隊列),前者負責做
互斥,后一個用于做線程同步。
3、談談對 Synchronized 關鍵字涉及到的類鎖,方法鎖,重入鎖的理解?
synchronized 修飾靜態方法獲取的是類鎖(類的字節碼文件對象)。
synchronized 修飾普通方法或代碼塊獲取的是對象鎖。這種機制確保了同一時刻
對于每一個類實例,其所有聲明為 synchronized 的成員函數中至多只有一個處
于可執行狀態,從而有效避免了類成員變量的訪問沖突。
它倆是不沖突的,也就是說:獲取了類鎖的線程和獲取了對象鎖的線程是不沖突
的!
public class Widget {
// 鎖住了
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
// 鎖住了
public synchronized void doSomething() {System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
因為鎖的持有者是“線程”,而不是“調用”。
線程 A 已經是有了 LoggingWidget 實例對象的鎖了,當再需要的時候可以繼續
**“開鎖”**進去的!
這就是內置鎖的可重入性。
4、wait、sleep 的區別和 notify 運行過程。
wait、sleep 的區別
最大的不同是在等待時 wait 會釋放鎖,而 sleep 一直持有鎖。wait 通常被用
于線程間交互,sleep 通常被用于暫停執行。
?
首先,要記住這個差別,“sleep 是 Thread 類的方法,wait 是 Object 類中
定義的方法”。盡管這兩個方法都會影響線程的執行行為,但是本質上是
有區別的。
? Thread.sleep 不會導致鎖行為的改變,如果當前線程是擁有鎖的,那么
Thread.sleep 不會讓線程釋放鎖。如果能夠幫助你記憶的話,可以簡單認
為和鎖相關的方法都定義在 Object 類中,因此調用 Thread.sleep 是不會
影響鎖的相關行為。
? Thread.sleep 和 Object.wait 都會暫停當前的線程,對于 CPU 資源來說,
不管是哪種方式暫停的線程,都表示它暫時不再需要 CPU 的執行時間。OS 會將執行時間分配給其它線程。區別是,調用 wait 后,需要別的線程
執行 notify/notifyAll 才能夠重新獲得 CPU 執行時間。
?
線程的狀態參考 Thread.State 的定義。新創建的但是沒有執行(還沒有
調用 start())的線程處于“就緒”,或者說 Thread.State.NEW 狀態。
? Thread.State.BLOCKED(阻塞)表示線程正在獲取鎖時,因為鎖不能獲取
到而被迫暫停執行下面的指令,一直等到這個鎖被別的線程釋放。
BLOCKED 狀態下線程,OS 調度機制需要決定下一個能夠獲取鎖的線程是
哪個,這種情況下,就是產生鎖的爭用,無論如何這都是很耗時的操作。
notify 運行過程
當線程 A(消費者)調用 wait()方法后,線程 A 讓出鎖,自己進入等待狀態,同
時加入鎖對象的等待隊列。 線程 B(生產者)獲取鎖后,調用 notify 方法通知
鎖對象的等待隊列,使得線程 A 從等待隊列進入阻塞隊列。 線程 A 進入阻塞隊
列后,直至線程 B 釋放鎖后,線程 A 競爭得到鎖繼續從 wait()方法后執行。
5、
synchronized 關鍵字和 Lock 的區別你知道嗎?為什么 Lock 的性能好一些?
類別
synchronized
Lock(底層實現主要是 Volatile + CAS)
存在
層次
Java 的關鍵字,在 jvm 層面上
是一個類
鎖的
釋放
1、已獲取鎖的線程執行完同步代碼,釋放鎖 2、線程
執行發生異常,jvm 會讓線程釋放鎖。
在 finally 中必須釋放鎖,不然容易造成線程死鎖。
鎖的
獲取
假設 A 線程獲得鎖,B 線程等待。如果 A 線程阻塞,B
分情況而定,Lock 有多個鎖獲取的方式,大致就是Lock(ReentrantLock)的底層實現主要是 Volatile + CAS(樂觀鎖),而
Synchronized 是一種悲觀鎖,比較耗性能。但是在 JDK1.6 以后對 Synchronized
的鎖機制進行了優化,加入了偏向鎖、輕量級鎖、自旋鎖、重量級鎖,在并發量
不大的情況下,性能可能優于 Lock 機制。所以建議一般請求并發量不大的情況
下使用 synchronized 關鍵字。
6、volatile 原理。
在《Java 并發編程:核心理論》一文中,我們已經提到可見性、有序性及原子性
問題,通常情況下我們可以通過 Synchronized 關鍵字來解決這些個問題,不過
如果對 Synchonized 原理有了解的話,應該知道 Synchronized 是一個較重量級
的操作,對系統的性能有比較大的影響,所以如果有其他解決方案,我們通常都
避免使用 Synchronized 來解決問題。
而 volatile 關鍵字就是 Java 中提供的另一種解決可見性有序性問題的方案。對于
原子性,需要強調一點,也是大家容易誤解的一點:對 volatile 變量的單次讀/
寫操作可保證原子性的,如 long 和 double 類型變量,但是并不能保證 i++這種
操作的原子性,因為本質上 i++是讀、寫兩次操作。
volatile 也是互斥同步的一種實現,不過它非常的輕量級。
volatile 的意義?
線程會一直等待。
可以嘗試獲得鎖,線程可以不用一直等待
鎖狀
無法判斷
可以判斷
鎖類
可重入 不可中斷 非公平
可重入 可判斷 可公平(兩者皆可)
性能
少量同步
大量同步?
防止 CPU 指令重排序
volatile 有兩條關鍵的語義:
保證被 volatile 修飾的變量對所有線程都是可見的
禁止進行指令重排序
要理解 volatile 關鍵字,我們得先從 Java 的線程模型開始說起。如圖所示:
Java 內存模型規定了所有字段(這些字段包括實例字段、靜態字段等,不包括局
部變量、方法參數等,因為這些是線程私有的,并不存在競爭)都存在主內存中,
每個線程會 有自己的工作內存,工作內存里保存了線程所使用到的變量在主內
存里的副本拷貝,線程對變量的操作只能在工作內存里進行,而不能直接讀寫主
內存,當然不同內存之間也 無法直接訪問對方的工作內存,也就是說主內存是
線程傳值的媒介。
我們來理解第一句話:
保證被 volatile 修飾的變量對所有線程都是可見的
如何保證可見性?
被 volatile 修飾的變量在工作內存修改后會被強制寫回主內存,其他線程在使用
時也會強制從主內存刷新,這樣就保證了一致性。
關于“保證被 volatile 修飾的變量對所有線程都是可見的”,有種常見的錯誤理解:?
由于 volatile 修飾的變量在各個線程里都是一致的,所以基于 volatile 變
量的運算在多線程并發的情況下是安全的。
這句話的前半部分是對的,后半部分卻錯了,因此它忘記考慮變量的操作是否具
有原子性這一問題。
舉個例子:
private volatile int start = 0;
private void volatile Keyword() {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
start++;
}
}
};
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
Log.d(TAG, "start = " + start);
}這段代碼啟動了 10 個線程,每次 10 次自增,按道理最終結果應該是 100,但是
結果并非如此。
為什么會這樣?
仔細看一下 start++,它其實并非一個原子操作,簡單來看,它有兩步:
1、取出 start 的值,因為有 volatile 的修飾,這時候的值是正確的。
2、自增,但是自增的時候,別的線程可能已經把 start 加大了,這種情況下就有
可能把較小的 start 寫回主內存中。 所以 volatile 只能保證可見性,在不符合以
下場景下我們依然需要通過加鎖來保證原子性:
?
運算結果并不依賴變量當前的值,或者只有單一線程修改變量的值。(要
么結果不依賴當前值,要么操作是原子性的,要么只要一個線程修改變量
的值)
?
變量不需要與其他狀態變量共同參與不變約束 比方說我們會在線程里加
個 boolean 變量,來判斷線程是否停止,這種情況就非常適合使用
volatile。
我們再來理解第二句話。
禁止進行指令重排序
什么是指令重排序?
指令重排序是指指令亂序執行,即在條件允許的情況下直接運行當前有能
力立即執行的后續指令,避開為獲取一條指令所需數據而造成的等待,通
過亂序執行的技術提供執行效率。
指令重排序會在被 volatile 修飾的變量的賦值操作前,添加一個內存屏障,
指令重排序時不能把后面的指令重排序移到內存屏障之前的位置。
7、synchronized 和 volatile 關鍵字的作用和區別。Volatile
1)保證了不同線程對這個變量進行操作時的可見性即一個線程修改了某個變量
的值,這新值對其他線程來是立即可見的。
2)禁止進行指令重排序。
作用
volatile 本質是在告訴 jvm 當前變量在寄存器(工作內存)中的值是不確定的,
需從主存中讀取;synchronized 則是鎖定當前變量,只有當前線程可以訪問該變
量,其它線程被阻塞住。
區別
1.volatile 僅能使用在變量級別;synchronized 則可以使用在變量、方法、和類
級別的。
2.volatile 僅能實現變量的修改可見性,并不能保證原子性;synchronized 則可
以保證變量的修改可見性和原子性。
3.volatile 不會造成線程的阻塞;synchronized 可能會造成線程的阻塞。
4.volatile 標記的變量不會被編譯器優化;synchronized 標記的變量可以被編譯
器優化。
8、ReentrantLock 的內部實現。
ReentrantLock 實現的前提就是 AbstractQueuedSynchronizer,簡稱 AQS,是
java.util.concurrent 的核心,CountDownLatch、FutureTask、Semaphore、
ReentrantLock 等都有一個內部類是這個抽象類的子類。由于 AQS 是基于 FIFO
隊列的實現,因此必然存在一個個節點,Node 就是一個節點,Node 有兩種模
式:共享模式和獨占模式。ReentrantLock 是基于 AQS 的,AQS 是 Java 并發包中眾多同步組件的構建基礎,它通過一個 int 類型的狀態變量 state 和一個 FIFO
隊列來完成共享資源的獲取,線程的排隊等待等。AQS 是個底層框架,采用模
板方法模式,它定義了通用的較為復雜的邏輯骨架,比如線程的排隊,阻塞,喚
醒等,將這些復雜但實質通用的部分抽取出來,這些都是需要構建同步組件的使
用者無需關心的,使用者僅需重寫一些簡單的指定的方法即可(其實就是對于共
享變量 state 的一些簡單的獲取釋放的操作)。AQS 的子類一般只需要重寫
tryAcquire(int arg)和 tryRelease(int arg)兩個方法即可。
ReentrantLock 的處理邏輯:
其內部定義了三個重要的靜態內部類,Sync,NonFairSync,FairSync。Sync 作
為 ReentrantLock 中公用的同步組件,繼承了 AQS(要利用 AQS 復雜的頂層邏
輯嘛,線程排隊,阻塞,喚醒等等);NonFairSync 和 FairSync 則都繼承 Sync,
調用 Sync 的公用邏輯,然后再在各自內部完成自己特定的邏輯(公平或非公平)。
接著說下這兩者的 lock()方法實現原理:
NonFairSync(非公平可重入鎖)
1.先獲取 state 值,若為 0,意味著此時沒有線程獲取到資源,CAS 將其設置為 1,
設置成功則代表獲取到排他鎖了;
2.若 state 大于 0,肯定有線程已經搶占到資源了,此時再去判斷是否就是自己
搶占的,是的話,state 累加,返回 true,重入成功,state 的值即是線程重入的
次數;
3.其他情況,則獲取鎖失敗。
FairSync(公平可重入鎖)
可以看到,公平鎖的大致邏輯與非公平鎖是一致的,不同的地方在于有
了!hasQueuedPredecessors()這個判斷邏輯,即便 state 為 0,也不能貿然直接去獲取,要先去看有沒有還在排隊的線程,若沒有,才能嘗試去獲取,做后面的處
理。反之,返回 false,獲取失敗。
最后,說下 ReentrantLock 的 tryRelease()方法實現原理:
若 state 值為 0,表示當前線程已完全釋放干凈,返回 true,上層的 AQS 會意識
到資源已空出。若不為 0,則表示線程還占有資源,只不過將此次重入的資源的
釋放了而已,返回 false。
ReentrantLock 是一種可重入的,可實現公平性的互斥鎖,它的設計基于 AQS
框架,可重入和公平性的實現邏輯都不難理解,每重入一次,state 就加 1,當
然在釋放的時候,也得一層一層釋放。至于公平性,在嘗試獲取鎖的時候多了一
個判斷:是否有比自己申請早的線程在同步隊列中等待,若有,去等待;若沒有,
才允許去搶占。
9、ReentrantLock 、synchronized 和 volatile 比較?
synchronized 是互斥同步的一種實現。
synchronized:當某個線程訪問被 synchronized 標記的方法或代碼塊時,這個
線程便獲得了該對象的鎖,其他線暫時無法訪問這個方法,只有等待這個方法執
行完畢或代碼塊執行完畢,這個線程才會釋放該對象的鎖,其他線程才能執行這
個方法代碼塊。
前面我們已經說了 volatile 關鍵字,這里我們舉個例子來綜合分析 volatile 與
synchronized 關鍵字的使用。
舉個例子:
public class Singleton {
// volatile 保證了:1 instance 在多線程并發的可見性 2 禁止 instance 在操作是的
指令重排序private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
// 第一次判空,保證不必要的同步
if (instance == null) {
// synchronized 對 Singleton 加全局鎖,保證每次只要一個線程創建實例
synchronized (Singleton.class) {
// 第二次判空時為了在 null 的情況下創建實例
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
這是一個經典的 DCL 單例。
它的字節碼如下:可以看到被 synchronized 同步的代碼塊,會在前后分別加上 monitorenter 和
monitorexit,這兩個字節碼都需要指定加鎖和解鎖的對象。
關于加鎖和解鎖的對象:
synchronized 代碼塊 :同步代碼塊,作用范圍是整個代碼塊,作用對象是調用
這個代碼塊的對象。
synchronized 方法 :同步方法,作用范圍是整個方法,作用對象是調用這個方
法的對象。
synchronized 靜態方法 :同步靜態方法,作用范圍是整個靜態方法,作用對象
是調用這個類的所有對象。synchronized(this):作用范圍是該對象中所有被 synchronized 標記的變量、方
法或代碼塊,作用對象是對象本身。
synchronized(ClassName.class) :作用范圍是靜態的方法或者靜態變量,作用對
象是 Class 對象。
synchronized(this)添加的是對象鎖,synchronized(ClassName.class)添加的是類
鎖,它們的區別如下:
對象鎖:Java 的所有對象都含有 1 個互斥鎖,這個鎖由 JVM 自動獲取和
釋放。線程進入 synchronized 方法的時候獲取該對象的鎖,當然如果已
經有線程獲取了這個對象的鎖那么當前線程會等待;
synchronized 方法正
常返回或者拋異常而終止,JVM 會自動釋放對象鎖。這里也體現了用
synchronized 來加鎖的好處,方法拋異常的時候,鎖仍然可以由 JVM 來
自動釋放。
類鎖:對象鎖是用來控制實例方法之間的同步,類鎖是來控制靜態方法(或
靜態變量互斥體)之間的同步。其實類鎖只是一個概念上的東西,并不是
真實存在的,它只用來幫助我們理解鎖定實例方法和靜態方法的區別的。
我們都知道,java 類可能會有很多個對象,但是只有 1 個 Class 對象,也
就說類的不同實例之間共享該類的 Class 對象。Class 對象其實也僅僅是 1
個 java 對象,只不過有點特殊而已。由于每個 java 對象都有個互斥鎖,
而類的靜態方法是需要 Class 對象。所以所謂類鎖,不過是 Class 對象的
鎖而已。獲取類的 Class 對象有好幾種,最簡單的就是 MyClass.class 的方
式。類鎖和對象鎖不是同一個東西,一個是類的 Class 對象的鎖,一個是
類的實例的鎖。也就是說:一個線程訪問靜態 sychronized 的時候,允許
另一個線程訪問對象的實例 synchronized 方法。反過來也是成立的,為
他們需要的鎖是不同的。?
三、其它 (???)
1、多線程的使用場景?
使用多線程就一定效率高嗎?有時候使用多線程并不是為了提高效率,而是使得
CPU 能同時處理多個事件。
為了不阻塞主線程,啟動其他線程來做事情,比如 APP 中的耗時操作都不在
UI 線程中做。
實現更快的應用程序,即主線程專門監聽用戶請求,子線程用來處理用戶請
求,以獲得大的吞吐量.感覺這種情況,多線程的效率未必高。這種情況下
的多線程是為了不必等待,可以并行處理多條數據。比如 JavaWeb 的就
是主線程專門監聽用戶的 HTTP 請求,然啟動子線程去處理用戶的 HTTP
請求。
某種雖然優先級很低的服務,但是卻要不定時去做。比如 Jvm 的垃圾回
收。
某種任務,雖然耗時,但是不消耗 CPU 的操作時間,開啟個線程,效率
會有顯著提高。比如讀取文件,然后處理。磁盤 IO 是個很耗費時間,但
是不耗 CPU 計算的工作。所以可以一個線程讀取數據,一個線程處理數
據。肯定比一個線程讀取數據,然后處理效率高。因為兩個線程的時候充
分利用了 CPU 等待磁盤 IO 的空閑時間。
2、CopyOnWriteArrayList 的了解。
Copy-On-Write 是什么?
在計算機中就是當你想要對一塊內存進行修改時,我們不在原有內存塊中進行寫
操作,而是將內存拷貝一份,在新的內存中進行寫操作,寫完之后呢,就將指向
原來內存指針指向新的內存,原來的內存就可以被回收掉。原理:
CopyOnWriteArrayList 這是一個 ArrayList 的線程安全的變體,
CopyOnWriteArrayList 底層實現添加的原理是先 copy 出一個容器(可以簡稱副
本),再往新的容器里添加這個新的數據,最后把新的容器的引用地址賦值給了
之前那個舊的的容器地址,但是在添加這個數據的期間,其他線程如果要去讀取
數據,仍然是讀取到舊的容器里的數據。
優點和缺點:
優點:
1.據一致性完整,為什么?因為加鎖了,并發數據不會亂。
2.解決了像 ArrayList、Vector 這種集合多線程遍歷迭代問題,記住,Vector 雖然
線程安全,只不過是加了 synchronized 關鍵字,迭代問題完全沒有解決!
缺點:
1.內存占有問題:很明顯,兩個數組同時駐扎在內存中,如果實際應用中,數據比
較多,而且比較大的情況下,占用內存會比較大,針對這個其實可以用
ConcurrentHashMap 來代替。
2.數據一致性:CopyOnWrite 容器只能保證數據的最終一致性,不能保證數據的
實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用
CopyOnWrite 容器。
使用場景:
1、讀多寫少(白名單,黑名單,商品類目的訪問和更新場景),為什么?因為
寫的時候會復制新集合。2、集合不大,為什么?因為寫的時候會復制新集合。
3、實時性要求不高,為什么,因為有可能會讀取到舊的集合數據。
3、ConcurrentHashMap 加鎖機制是什么,詳細說一下?
Java7 ConcurrentHashMap
ConcurrentHashMap 作為一種線程安全且高效的哈希表的解決方案,尤其其中
的"分段鎖"的方案,相比 HashTable 的表鎖在性能上的提升非常之大。HashTable
容器在競爭激烈的并發環境下表現出效率低下的原因,是因為所有訪問
HashTable 的線程都必須競爭同一把鎖,那假如容器里有多把鎖,每一把鎖用于
鎖容器其中一部分數據,那么當多線程訪問容器里不同數據段的數據時,線程間
就不會存在鎖競爭,從而可以有效的提高并發訪問效率,這就是
ConcurrentHashMap 所使用的鎖分段技術,首先將數據分成一段一段的存儲,
然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其
他段的數據也能被其他線程訪問。
ConcurrentHashMap 是一個 Segment 數組,Segment 通過繼承
ReentrantLock 來進行加鎖,所以每次需要加鎖的操作鎖住的是一個 segment,
這樣只要保證每個 Segment 是線程安全的,也就實現了全局的線程安全。
concurrencyLevel:并行級別、并發數、Segment 數。默認是 16,也就是說
ConcurrentHashMap 有 16 個 Segments,所以理論上,這個時候,最多可以
同時支持 16 個線程并發寫,只要它們的操作分別分布在不同的 Segment 上。
這個值可以在初始化的時候設置為其他值,但是一旦初始化以后,它是不可以擴
容的。其中的每個 Segment 很像 HashMap,不過它要保證線程安全,所以處
理起來要麻煩些。
初始化槽: ensureSegmentConcurrentHashMap 初始化的時候會初始化第一個槽 segment[0],對于其他槽
來說,在插入第一個值的時候進行初始化。對于并發操作使用 CAS 進行控制。
Java8 ConcurrentHashMap
拋棄了原有的 Segment 分段鎖,而采用了 CAS + synchronized 來保證并發安
全性。結構上和 Java8 的 HashMap(數組+鏈表+紅黑樹) 基本上一樣,不過
它要保證線程安全性,所以在源碼上確實要復雜一些。1.8 在 1.7 的數據結構上
做了大的改動,采用紅黑樹之后可以保證查詢效率(O(logn)),甚至取消了
ReentrantLock 改為了 synchronized,這樣可以看出在新版的 JDK 中對
synchronized 優化是很到位的。
4、線程死鎖的 4 個條件?
死鎖是如何發生的,如何避免死鎖?
當線程 A 持有獨占鎖 a,并嘗試去獲取獨占鎖 b 的同時,線程 B 持有獨占鎖 b,
并嘗試獲取獨占鎖 a 的情況下,就會發生 AB 兩個線程由于互相持有對方需要的
鎖,而發生的阻塞現象,我們稱為死鎖。
public class DeadLockDemo {
public static void main(String[] args) {
// 線程 a
Thread td1 = new Thread(new Runnable() {
public void run() {
DeadLockDemo.method1();
}
});// 線程 b
Thread td2 = new Thread(new Runnable() {
public void run() {
DeadLockDemo.method2();
}
});
td1.start();
td2.start();
}
public static void method1() {
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程 a 嘗試獲取 integer.class");
synchronized (Integer.class) {
}
}
}public static void method2() {
synchronized (Integer.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程 b 嘗試獲取 String.class");
synchronized (String.class) {
}
}
}
}
造成死鎖的四個條件:
?
互斥條件:一個資源每次只能被一個線程使用。
?
請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不
放。
?
不剝奪條件:線程已獲得的資源,在未使用完之前,不能強行剝奪。
?
循環等待條件:若干線程之間形成一種頭尾相接的循環等待資源關系。
在并發程序中,避免了邏輯中出現數個線程互相持有對方線程所需要的獨占鎖的
的情況,就可以避免死鎖,如下所示:
public class BreakDeadLockDemo {
public static void main(String[] args) {
// 線程 a
Thread td1 = new Thread(new Runnable() {public void run() {
DeadLockDemo2.method1();
}
});
// 線程 b
Thread td2 = new Thread(new Runnable() {
public void run() {
DeadLockDemo2.method2();
}
});
td1.start();
td2.start();
}
public static void method1() {
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程 a 嘗試獲取 integer.class");
synchronized (Integer.class) {
System.out.println("線程 a 獲取到 integer.class");}
}
}
public static void method2() {
// 不再獲取線程 a 需要的 Integer.class 鎖。
synchronized (String.class) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程 b 嘗試獲取 Integer.class");
synchronized (Integer.class) {
System.out.println("線程 b 獲取到 Integer.class");
}
}
}
}
5、CAS 介紹?
UnsafeUnsafe 是 CAS 的核心類。因為 Java 無法直接訪問底層操作系統,而是通過本地
(native)方法來訪問。不過盡管如此,JVM 還是開了一個后門,JDK 中有一個
類 Unsafe,它提供了硬件級別的原子操作。
CAS
CAS,Compare and Swap 即比較并交換,設計并發算法時常用到的一種技術,
java.util.concurrent 包全完建立在 CAS 之上,沒有 CAS 也就沒有此包,可見 CAS
的重要性。當前的處理器基本都支持 CAS,只不過不同的廠家的實現不一樣罷了。
并且 CAS 也是通過 Unsafe 實現的,由于 CAS 都是硬件級別的操作,因此效率
會比普通加鎖高一些。
CAS 的缺點
CAS 看起來很美,但這種操作顯然無法涵蓋并發下的所有場景,并且 CAS 從語
義上來說也不是完美的,存在這樣一個邏輯漏洞:如果一個變量 V 初次讀取的
時候是 A 值,并且在準備賦值的時候檢查到它仍然是 A 值,那我們就能說明它
的值沒有被其他線程修改過了嗎?如果在這段期間它的值曾經被改成了 B,然后
又改回 A,那 CAS 操作就會誤認為它從來沒有被修改過。這個漏洞稱為 CAS 操
作的"ABA"問題。java.util.concurrent 包為了解決這個問題,提供了一個帶有標
記的原子引用類"AtomicStampedReference",它可以通過控制變量值的版本來
保證 CAS 的正確性。不過目前來說這個類比較"雞肋",大部分情況下 ABA 問題
并不會影響程序并發的正確性,如果需要解決 ABA 問題,使用傳統的互斥同步
可能回避原子類更加高效。
6、進程和線程的區別?
簡而言之,一個程序至少有一個進程,一個進程至少有一個線程。1、線程的劃分尺度小于進程,使得多線程程序的并發性高。
2、進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而
極大地提高了程序的運行效率。
3、線程在執行過程中與進程還是有區別的。每個獨立的線程有一個程序
運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必
須依存在應用程序中,由應用程序提供多個線程執行控制。
4、從邏輯角度來看,多線程的意義在于一個應用程序中,有多個執行部
分可以同時執行。但操作系統并沒有將多個線程看做多個獨立的應用,來
實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。
5、進程是具有一定獨立功能的程序關于某個數據集合上的一次運行活動,
進程是系統進行資源分配和調度的一個獨立單位。線程是進程的一個實體,
是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.
線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如
程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共
享進程所擁有的全部資源。
6、一個線程可以創建和撤銷另一個線程;同一個進程中的多個線程之間可
以并發執行。
7、進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它
進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆
棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等于整
個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,
耗費資源較大,效率要差一些。
7、什么導致線程阻塞?
線程的阻塞為了解決對共享存儲區的訪問沖突,Java 引入了同步機制,現在讓我們來考察
多個線程對共享資源的訪問,顯然同步機制已經不夠了,因為在任意時刻所要求
的資源不一定已經準備好了被訪問,反過來,同一時刻準備好了的資源也可能不
止一個。為了解決這種情況下的訪問控制問題,Java 引入了對阻塞機制的支持.
阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒),學過操
作系統的同學對它一定已經很熟悉了。Java 提供了大量方法來支持阻塞,下面
讓我們逐一分析。
sleep() 方法:sleep() 允許 指定以毫秒為單位的一段時間作為參數,它使得線
程在指定的時間內進入阻塞狀態,不能得到 CPU 時間,指定的時間一過,線程
重新進入可執行狀態。 典型地,sleep() 被用在等待某個資源就緒的情形:測試
發現條件不滿足后,讓線程阻塞一段時間后重新測試,直到條件滿足為止。
suspend() 和 resume() 方法:兩個方法配套使用,suspend()使得線程進入阻塞
狀態,并且不會自動恢復,必須其對應的 resume() 被調用,才能使得線程重新
進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個線程產生
的結果的情形:測試發現結果還沒有產生后,讓線程阻塞,另一個線程產生了結
果后,調用 resume() 使其恢復。
yield() 方法:yield() 使得線程放棄當前分得的 CPU 時間,但是不使線程阻塞,
即線程仍處于可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果
等價于調度程序認為該線程已執行了足夠的時間從而轉到另一個線程。
wait() 和 notify() 方法:兩個方法配套使用,wait() 使得線程進入阻塞狀態,它
有兩種形式,一種允許指定以毫秒為單位的一段時間作為參數,另一種沒有參數,
前者當對應的 notify() 被調用或者超出指定時間時線程重新進入可執行狀態,
后者則必須對應的 notify() 被調用。初看起來它們與 suspend() 和 resume()
方法對沒有什么分別,但是事實上它們是截然不同的。區別的核心在于,前面敘述的所有方法,阻塞時都不會釋放占用的鎖(如果占用了的話),而這一對方法
則相反。
上述的核心區別導致了一系列的細節上的區別。
首先,前面敘述的所有方法都隸屬于 Thread 類,但是這一對卻直接隸屬于
Object 類,也就是說,所有對象都擁有這一對方法。初看起來這十分不可思議,
但是實際上卻是很自然的,因為這一對方法阻塞時要釋放占用的鎖,而鎖是任何
對象都具有的,調用任意對象的 wait() 方法導致線程阻塞,并且該對象上的鎖
被釋放。而調用 任意對象的 notify()方法則導致因調用該對象的 wait() 方法而
阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖后才真正可執行)。
其次,前面敘述的所有方法都可在任何位置調用,但是這一對方法卻必須在
synchronized 方法或塊中調用,理由也很簡單,只有在 synchronized 方法或塊
中當前線程才占有鎖,才有鎖可以釋放。同樣的道理,調用這一對方法的對象上
的鎖必須為當前線程所擁有,這樣才有鎖可以釋放。因此,這一對方法調用必須
放置在這樣的 synchronized 方法或塊中,該方法或塊的上鎖對象就是調用這一
對方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但在運行時會出現
IllegalMonitorStateException 異常。
wait() 和 notify() 方法的上述特性決定了它們經常和 synchronized 方法或塊一
起使用,將它們和操作系統的進程間通信機制作一個比較就會發現它們的相似
性:synchronized 方法或塊提供了類似于操作系統原語的功能,它們的執行不會
受到多線程機制的干擾,而這一對方法則相當于 block 和 wakeup 原語(這一
對方法均聲明為 synchronized)。它們的結合使得我們可以實現操作系統上一
系列精妙的進程間通信的算法(如信號量算法),并用于解決各種復雜的線程間
通信問題。(此外,線程間通信的方式還有多個線程通過 synchronized 關鍵字
這種方式來實現線程間的通信、while 輪詢、使用 java.io.PipedInputStream 和
java.io.PipedOutputStream 進行通信的管道通信)。
關于 wait() 和 notify() 方法最后再說明兩點:第一:調用 notify() 方法導致解除阻塞的線程是從調用該對象的 wait() 方法而
阻塞的線程中隨機選取的,我們無法預料哪一個線程將會被選擇,所以編程時要
特別小心,避免因這種不確定性而產生問題。
第二:除了 notify(),還有一個方法 notifyAll() 也可起到類似作用,唯一的區別
在于,調用 notifyAll() 方法將把因調用該對象的 wait() 方法而阻塞的所有線程
一次性全部解除阻塞。當然,只有獲得鎖的那一個線程才能進入可執行狀態。
談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend() 方法和不指定
超時期限的 wait() 方法的調用都可能產生死鎖。遺憾的是,Java 并不在語言級
別上支持死鎖的避免,我們在編程中必須小心地避免死鎖。
以上我們對 Java 中實現線程阻塞的各種方法作了一番分析,我們重點分析了
wait() 和 notify() 方法,因為它們的功能最強大,使用也最靈活,但是這也導致
了它們的效率較低,較容易出錯。實際使用中我們應該靈活使用各種方法,以便
更好地達到我們的目的。
8、線程的生命周期
線程狀態流程圖? NEW:創建狀態,線程創建之后,但是還未啟動。
? RUNNABLE:運行狀態,處于運行狀態的線程,但有可能處于等待狀態,
例如等待 CPU、IO 等。
? WAITING:等待狀態,一般是調用了 wait()、join()、LockSupport.spark()
等方法。
? TIMED_WAITING:超時等待狀態,也就是帶時間的等待狀態。一般是調
用 了 wait(time) 、 join(time) 、 LockSupport.sparkNanos() 、
LockSupport.sparkUnit()等方法。
? BLOCKED:阻塞狀態,等待鎖的釋放,例如調用了 synchronized 增加了
鎖。
? TERMINATED:終止狀態,一般是線程完成任務后退出或者異常終止。NEW、WAITING、TIMED_WAITING 都比較好理解,我們重點說一說 RUNNABLE
運行態和 BLOCKED 阻塞態。
線程進入 RUNNABLE 運行態一般分為五種情況:
?
線程調用 sleep(time)后結束了休眠時間
?
線程調用的阻塞 IO 已經返回,阻塞方法執行完畢
?
線程成功的獲取了資源鎖
?
線程正在等待某個通知,成功的獲得了其他線程發出的通知
?
線程處于掛起狀態,然后調用了 resume()恢復方法,解除了掛起。
線程進入 BLOCKED 阻塞態一般也分為五種情況:
?
線程調用 sleep()方法主動放棄占有的資源
?
線程調用了阻塞式 IO 的方法,在該方法返回前,該線程被阻塞。
?
線程視圖獲得一個資源鎖,但是該資源鎖正被其他線程鎖持有。
?
線程正在等待某個通知
?
線程調度器調用 suspend()方法將該線程掛起
我們再來看看和線程狀態相關的一些方法。
sleep()方法讓當前正在執行的線程在指定時間內暫停執行,正在執行的線
程可以通過 Thread.currentThread()方法獲取。
yield()方法放棄線程持有的 CPU 資源,將其讓給其他任務去占用 CPU 執
行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得 CPU 時間
片。wait()方法是當前執行代碼的線程進行等待,將當前線程放入預執行隊列,
并在 wait()所在的代碼處停止執行,直到接到通知或者被中斷為止。該方
法可以使得調用該方法的線程釋放共享資源的鎖,然后從運行狀態退出,
進入等待隊列,直到再次被喚醒。該方法只能在同步代碼塊里調用,否則
會拋出 IllegalMonitorStateException 異常。wait(long millis)方法等待某
一段時間內是否有線程對鎖進行喚醒,如果超過了這個時間則自動喚醒。
notify()方法用來通知那些可能等待該對象的對象鎖的其他線程,該方法
可以隨機喚醒等待隊列中等同一共享資源的一個線程,并使該線程退出等
待隊列,進入可運行狀態。
notifyAll()方法可以使所有正在等待隊列中等待同一共享資源的全部線程
從等待狀態退出,進入可運行狀態,一般會是優先級高的線程先執行,但
是根據虛擬機的實現不同,也有可能是隨機執行。
join()方法可以讓調用它的線程正常執行完成后,再去執行該線程后面的
代碼,它具有讓線程排隊的作用。
9、樂觀鎖與悲觀鎖。
悲觀鎖
總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數
據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每
次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)。Java
中 synchronized 和 ReentrantLock 等獨占鎖就是悲觀鎖思想的實現。
樂觀鎖
總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,
但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版
本號機制和 CAS 算法實現。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐
量。在 Java 中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀
鎖的一種實現方式 CAS 實現的。使用場景
樂觀鎖適用于寫比較少的情況下(多讀場景),而一般多寫的場景下用悲觀鎖就
比較合適。
樂觀鎖常見的兩種實現方式
1、版本號機制
一般是在數據表中加上一個數據版本號 version 字段,表示數據被修改的次數,
當數據被修改時,version 值會加 1。當線程 A 要更新數據值時,在讀取數據的
同時也會讀取 version 值,在提交更新時,若剛才讀取到的 version 值為當前數
據庫中的 version 值相等時才更新,否則重試更新操作,直到更新成功。
2、CAS 算法
即 compare and swap(比較與交換),是一種有名的無鎖算法。CAS 有 3 個操
作數,內存值 V,舊的預期值 A,要修改的新值 B。當且僅當預期值 A 和內存值
V 相同時,將內存值 V 修改為 B,否則什么都不做。 一般情況下是一個自旋操
作,即不斷的重試。
樂觀鎖的缺點
1、ABA 問題
如果一個變量 V 初次讀取的時候是 A 值,并且在準備賦值的時候檢查到它仍然
是 A 值,那我們就能說明它的值沒有被其他線程修改過了嗎?很明顯是不能的,
因為在這段時間它的值可能被改為其他值,然后又改回 A,那 CAS 操作就會誤
認為它從來沒有被修改過。這個問題被稱為 CAS 操作的 "ABA"問題。JDK 1.5 以后的 AtomicStampedReference 類一定程度上解決了這個問題,其
中的 compareAndSet 方法就是首先檢查當前引用是否等于預期引用,并且當前
標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值設
置為給定的更新值。
2、自旋 CAS(也就是不成功就一直循環執行直到成功)如果長時間不成功,會
給 CPU 帶來非常大的執行開銷。
3、CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。但
是從 JDK 1.5 開始,提供了 AtomicReference 類來保證引用對象之間的原子性,
你可以把多個變量放在一個對象里來進行 CAS 操作.所以我們可以使用鎖或者
利用 AtomicReference 類把多個共享變量合并成一個共享變量來操作。
10、run()和 start()方法區別?
1.start()方法來啟動線程,真正實現了多線程運行,這時無需等待 run 方法體代
碼執行完畢而直接繼續執行下面的代碼:
通過調用Thread類的 start()方法來啟動一個線程,這時此線程是處于就緒狀態,
并沒有運行。 然后通過此 Thread 類調用方法 run()來完成其運行操作的, 這里
方法 run()稱為線程體, 它包含了要執行的這個線程的內容, Run 方法運行結
束, 此線程終止, 而 CPU 再運行其它線程,在 Android 中一般是主線程。
2.run()方法當作普通方法的方式調用,程序還是要順序執行,還是要等待 run 方
法體執行完畢后才可繼續執行下面的代碼:
而如果直接用 Run 方法, 這只是調用一個方法而已, 程序中依然只有主線程
--這一個線程,其程序執行路徑還是只有一條,這樣就沒有達到寫線程的目的。
11、多線程斷點續傳原理。在本地下載過程中要使用數據庫實時存儲到底存儲到文件的哪個位置了,這樣點
擊開始繼續傳遞時,才能通過 HTTP 的 GET 請求中的
setRequestProperty("Range","bytes=startIndex-endIndex");方法可以告訴服務
器,數據從哪里開始,到哪里結束。同時在本地的文件寫入時,RandomAccessFile
的 seek()方法也支持在文件中的任意位置進行寫入操作。同時通過廣播或事件總
線機制將子線程的進度告訴 Activity 的進度條。關于斷線續傳的 HTTP 狀態碼是
206,即 HttpStatus.SC_PARTIAL_CONTENT。
12、怎么安全停止一個線程任務?原理是什么?線程池里有類似機制嗎?
終止線程
1、使用 violate boolean 變量退出標志,使線程正常退出,也就是當 run 方法完
成后線程終止。(推薦)
2、使用 interrupt()方法中斷線程,但是線程不一定會終止。
3、使用 stop 方法強行終止線程。不安全主要是:thread.stop()調用之后,創建
子線程的線程就會拋出 ThreadDeatherror 的錯誤,并且會釋放子線程所持有的
所有鎖。
終止線程池
ExecutorService 線程池就提供了 shutdown 和 shutdownNow 這樣的生命周期方
法來關閉線程池自身以及它擁有的所有線程。
1、shutdown 關閉線程池
線程池不會立刻退出,直到添加到線程池中的任務都已經處理完成,才會退出。
2、shutdownNow 關閉線程池并中斷任務終止等待執行的線程,并返回它們的列表。試圖停止所有正在執行的線程,試圖
終止的方法是調用 Thread.interrupt(),但是大家知道,如果線程中沒有 sleep 、
wait、Condition、定時鎖等應用, interrupt()方法是無法中斷當前的線程的。所以,
ShutdownNow()并不代表線程池就一定立即就能退出,它可能必須要等待所有
正在執行的任務都執行完成了才能退出。
13、堆內存,棧內存理解,棧如何轉換成堆?
?
在函數中定義的一些基本類型的變量和對象的引用變量都是在函數的棧
內存中分配。
?
堆內存用于存放由 new 創建的對象和數組。JVM 里的“堆”(heap)特指
用于存放 Java 對象的內存區域。所以根據這個定義,Java 對象全部都在
堆上。JVM 的堆被同一個 JVM 實例中的所有 Java 線程共享。它通常由某
種自動內存管理機制所管理,這種機制通常叫做“垃圾回收”(garbage
collection,GC)。
?
堆主要用來存放對象的,棧主要是用來執行程序的。
?
實際上,棧中的變量指向堆內存中的變量,這就是 Java 中的指針!
14、如何控制某個方法允許并發訪問線程的個數;
15、多進程開發以及多進程應用場景;
16、Java 的線程模型;
17、死鎖的概念,怎么避免死鎖?
18、如何保證多線程讀寫文件的安全?
19、線程如何關閉,以及如何防止線程的內存泄漏?
20、為什么要有線程,而不是僅僅用進程?
21、多個線程如何同時請求,返回的結果如何等待所有線程數據完成后合成一個數據?
22、線程如何關閉?23、數據一致性如何保證?
24、兩個進程同時要求寫或者讀,能不能實現?如何防止進程的同步?
25、談談對多線程的理解并舉例說明
26、線程的狀態和優先級。
27、ThreadLocal 的使用
28、Java 中的并發工具(CountDownLatch,CyclicBarrier 等)
29、進程線程在操作系統中的實現
30、雙線程通過線程同步的方式打印 12121212.......
31、java 線程,場景實現,多個線程如何同時請求,返回的結果如何等待所有線程數據完成后
合成一個數據
32、服務器只提供數據接收接口,在多線程或多進程條件下,如何保證數據的有序到達?
33、單機上一個線程池正在處理服務,如果忽然斷電了怎么辦(正在處理和阻塞隊列里的請求
怎么處理)?
第三節 Java 虛擬機面試題 (???)
1、JVM 內存區域。JVM 基本構成
從上圖可知,JVM 主要包括四個部分:
1.類加載器(ClassLoader):在 JVM 啟動時或者在類運行將需要的 class 加載到
JVM 中。(下圖表示了從 java 源文件到 JVM 的整個過程,可配合理解。2.執行引擎:負責執行 class 文件中包含的字節碼指令;
3.內存區(也叫運行時數據區):是在 JVM 運行的時候操作所分配的內存區。
運行時內存區主要可以劃分為 5 個區域,如圖:方法區(MethodArea):用于存儲類結構信息的地方,包括常量池、靜態常量、
構造函數等。雖然 JVM 規范把方法區描述為堆的一個輯部分, 但它卻有個別名
non-heap(非堆),所以大家不要搞混淆了。方法區還包含一個運行時常量池。
java 堆(Heap):存儲 java 實例或者對象的地方。這塊是 GC 的主要區域。從存儲
的內容我們可以很容易知道,方法和堆是被所有 java 線程共享的。
java 棧(Stack):java 棧總是和線程關聯在一起,每當創一個線程時,JVM 就會為
這個線程創建一個對應的 java 棧在這個 java 棧中,其中又會包含多個棧幀,每運
行一個方法就建一個棧幀,用于存儲局部變量表、操作棧、方法返回等。每一個
方法從調用直至執行完成的過程,就對應一棧幀在 java 棧中入棧到出棧的過程。
所以 java 棧是現成有的。程序計數器(PCRegister):用于保存當前線程執行的內存地址。由于 JVM 程序是
多線程執行的(線程輪流切換),所以為了保證程切換回來后,還能恢復到原先
狀態,就需要一個獨立計數器,記錄之前中斷的地方,可見程序計數器也是線程
私有的。
本地方法棧(Native MethodStack):和 java 棧的作用差不多,只不過是為 JVM 使
用到 native 方法服務的。
4.本地方法接口:主要是調用 C 或 C++實現的本地方法及回調結果。
開線程影響哪塊內存?
每當有線程被創建的時候,JVM 就需要為其在內存中分配虛擬機棧和本地方法
棧來記錄調用方法的內容,分配程序計數器記錄指令執行的位置,這樣的內存消
耗就是創建線程的內存代價。
2、JVM 的內存模型的理解?
Java內存模型即Java Memory Model,簡稱JMM。
JMM定義了Java 虛擬機(JVM)
在計算機內存(RAM)中的工作方式。JVM 是整個計算機虛擬模型,所以 JMM 是
隸屬于 JVM 的。
Java 線程之間的通信總是隱式進行,并且采用的是共享內存模型。這里提到的共
享內存模型指的就是 Java 內存模型(簡稱 JMM),JMM 決定一個線程對共享變量
的寫入何時對另一個線程可見。從抽象的角度來看,JMM 定義了線程和主內存
之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每
個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以
讀/寫共享變量的副本。本地內存是 JMM 的一個抽象概念,并不真實存在。它涵
蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。總之,JMM 就是一組規則,這組規則意在解決在并發編程可能出現的線程安全
問題,并提供了內置解決方案(happen-before 原則)及其外部可使用的同步手
段(synchronized/volatile 等),確保了程序執行在多線程環境中的應有的原子性,
可視性及其有序性。
需要更全面理解建議閱讀以下文章:
全面理解 Java 內存模型(JMM)及 volatile 關鍵字
全面理解 Java 內存模型
3、描述一下 GC 的原理和回收策略?
提到垃圾回收,我們可以先思考一下,如果我們去做垃圾回收需要解決哪些問
題?
一般說來,我們要解決三個問題:
1、回收哪些內存?
2、什么時候回收?
3、如何回收?
這些問題分別對應著引用管理和回收策略等方案。
提到引用,我們都知道 Java 中有四種引用類型:
?
強引用:代碼中普遍存在的,只要強引用還存在,垃圾收集器就不會回收
掉被引用的對象。
?
軟引用:SoftReference,用來描述還有用但是非必須的對象,當內存不足
的時候會回收這類對象。?
弱引用:WeakReference,用來描述非必須對象,弱引用的對象只能生存
到下一次 GC 發生時,當 GC 發生時,無論內存是否足夠,都會回收該對
象。
?
虛引用:PhantomReference,一個對象是否有虛引用的存在,完全不會
對其生存時間產生影響,也無法通過虛引用取得一個對象的引用,它存在
的唯一目的是在這個對象被回收時可以收到一個系統通知。
不同的引用類型,在做 GC 時會區別對待,我們平時生成的 Java 對象,默認都
是強引用,也就是說只要強引用還在,GC 就不會回收,那么如何判斷強引用是
否存在呢?
一個簡單的思路就是:引用計數法,有對這個對象的引用就+1,不再引用就-1,
但是這種方式看起來簡單美好,但它卻不能解決循環引用計數的問題。
因此可達性分析算法登上歷史舞臺,用它來判斷對象的引用是否存在。
可達性分析算法通過一系列稱為 GCRoots 的對象作為起始點,從這些節點從上
向下搜索,所走過的路徑稱為引用鏈,當一個對象沒有任何引用鏈與 GCRoots
連接時就說明此對象不可用,也就是對象不可達。
GC Roots 對象通常包括:
?
虛擬機棧中引用的對象(棧幀中的本地變量表)
?
方法中類的靜態屬性引用的對象
?
方法區中常量引用的對象
? Native 方法引用的對象
可達性分析算法整個流程如下所示:
第一次標記:對象在經過可達性分析后發現沒有與 GC Roots 有引用鏈,則進行
第一次標記并進行一次篩選,篩選條件是:該對象是否有必要執行 finalize()方法。
沒有覆蓋 finalize()方法或者 finalize()方法已經被執行過都會被認為沒有必要執行。 如果有必要執行:則該對象會被放在一個 F-Queue 隊列,并稍后在由虛擬
機建立的低優先級 Finalizer 線程中觸發該對象的 finalize()方法,但不保證一定等
待它執行結束,因為如果這個對象的 finalize()方法發生了死循環或者執行時間較
長的情況,會阻塞 F-Queue 隊列里的其他對象,影響 GC。
第二次標記:GC 對 F-Queue 隊列里的對象進行第二次標記,如果在第二次標記
時該對象又成功被引用,則會被移除即將回收的集合,否則會被回收。
總之,JVM 在做垃圾回收的時候,會檢查堆中的所有對象否會被這些根集對象
引用,不能夠被引用的對象就會被圾收集器回收。一般回收算法也有如下幾種:
1).標記-清除(Mark-sweep)
標記-清除算法采用從根集合進行掃描,對存活的對象進行標記,標記完畢后,
再掃描整個空間中未被標記的對象,進行回收。標記-清除算法不需要進行對象
的移動,并且僅對不存活的對象進行處理,在存活對象比較多的情況下極為高效,
但由于標記-清除算法直接回收不存活的對象,因此會造成內存碎片。
2).標記-整理(Mark-Compact)
標記-整理算法采用標記-清除算法一樣的方式進行對象的標記,但在清除時不
同,在回收不存活的對象占用的空間后,會將所有的存活對象往左端空閑空間移
動,并更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了
對象的移動,因此成本更高,但是卻解決了內存碎片的問題。該垃圾回收算法適
用于對象存活率高的場景(老年代)。
3).復制(Copying)
復制算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。
當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已
使用過的內存空間一次清理掉。這種算法適用于對象存活率低的場景,比如新生代。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存
碎片等復雜情況。
4).分代收集算法
不同的對象的生命周期(存活情況)是不一樣的,而不同生命周期的對象位于堆中
不同的區域,因此對堆內存不同區域采用不同的策略進行回收可以提高 JVM 的
執行效率。當代商用虛擬機使用的都是分代收集算法:新生代對象存活率低,就
采用復制算法;老年代存活率高,就用標記清除算法或者標記整理算法。Java
堆內存一般可以分為新生代、老年代和永久代三個模塊:
新生代:
1.所有新生成的對象首先都是放在新生代的。新生代的目標就是盡可能快速的收
集掉那些生命周期短的對象。
2.新生代內存按照 8:1:1 的比例分為一個 eden 區和兩個
survivor(survivor0,survivor1)區。大部分對象在 Eden 區中生成。回收時先將 eden
區存活對象復制到一個 survivor0 區,然后清空 eden 區,當這個 survivor0 區也
存放滿了時,則將 eden 區和 survivor0 區存活對象復制到另一個 survivor1 區,
然后清空 eden 和這個 survivor0 區,此時 survivor0 區是空的,然后將 survivor0
區和 survivor1 區交換,即保持 survivor1 區為空, 如此往復。
3.當 survivor1 區不足以存放 eden 和 survivor0 的存活對象時,就將存活對象直
接存放到老年代。若是老年代也滿了就會觸發一次 Full GC,也就是新生代、老
年代都進行回收。
4.新生代發生的 GC 也叫做 Minor GC,MinorGC 發生頻率比較高(不一定等 Eden
區滿了才觸發)。老年代:
1.在老年代中經歷了 N 次垃圾回收后仍然存活的對象,就會被放到老年代中。因
此,可以認為老年代中存放的都是一些生命周期較長的對象。
2.內存比新生代也大很多(大概比例是 1:2),當老年代內存滿時觸發 Major GC,
即 Full GC。Full GC 發生頻率比較低,老年代對象存活時間比較長。
永久代:
永久代主要存放靜態文件,如 Java 類、方法等。永久代對垃圾回收沒有顯著影
響,但是有些應用可能動態生成或者調用一些 class,例如使用反射、動態代理、
CGLib 等 bytecode 框架時,在這種時候需要設置一個比較大的永久代空間來存
放這些運行過程中新增的類。
垃圾收集器
垃圾收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現:
Serial 收集器(復制算法): 新生代單線程收集器,標記和清理都是單線程,
優點是簡單高效;
Serial Old 收集器 (標記-整理算法): 老年代單線程收集器,Serial 收集器
的老年代版本;
ParNew 收集器 (復制算法): 新生代收并行集器,實際上是 Serial 收集器
的多線程版本,在多核 CPU 環境下有著比 Serial 更好的表現;
CMS(Concurrent Mark Sweep)收集器(標記-清除算法): 老年代并行
收集器,以獲取最短回收停頓時間為目標的收集器,具有高并發、低停頓
的特點,追求最短 GC 回收停頓時間。Parallel Old 收集器 (標記-整理算法): 老年代并行收集器,吞吐量優先,
Parallel Scavenge 收集器的老年代版本;
Parallel Scavenge 收集器 (復制算法): 新生代并行收集器,追求高吞吐量,
高效利用 CPU。吞吐量 = 用戶線程時間/(用戶線程時間+GC 線程時間),
高吞吐量可以高效率的利用 CPU 時間,盡快完成程序的運算任務,適合
后臺應用等對交互相應要求不高的場景;
G1(Garbage First)收集器 (標記-整理算法): Java 堆并行收集器,G1 收
集器是 JDK1.7 提供的一個新收集器,G1 收集器基于“標記-整理”算法實
現,也就是說不會產生內存碎片。此外,G1 收集器不同于之前的收集器
的一個重要特點是:G1 回收的范圍是整個 Java 堆(包括新生代,老年代),
而前六種收集器回收的范圍僅限于新生代或老年代。
內存分配和回收策略
JAVA 自動內存管理:給對象分配內存 以及 回收分配給對象的內存。
1、對象優先在 Eden 分配,當 Eden 區沒有足夠空間進行分配時,虛擬機將發起
一次 MinorGC。
2、大對象直接進入老年代。如很長的字符串以及數組。很長的字符串以及數組。
3、長期存活的對象將進入老年代。當對象在新生代中經歷過一定次數(默認為
15)的 Minor GC 后,就會被晉升到老年代中。
4、動態對象年齡判定。為了更好地適應不同程序的內存狀況,虛擬機并不是永
遠地要求對象年齡必須達到了 MaxTenuringThreshold 才能晉升老年代,如果在Survivor 空間中相同年齡所有對象大小的總和大于 Survivor 空間的一半,年齡大
于或等于該年齡的對象就可以直接進入老年代,無須等到
MaxTenuringThreshold 中要求的年齡。
需要更全面的理解請點擊這里
4、類的加載器,雙親機制,Android 的類加載器。
類的加載器
大家都知道,一個 Java 程序都是由若干個.class 文件組織而成的一個完整的 Java
應用程序,當程序在運行時,即會調用該程序的一個入口函數來調用系統的相關
功能,而這些功能都被封裝在不同的 class 文件當中,所以經常要從這個 class
文件中要調用另外一個 class 文件中的方法,如果另外一個文件不存在的話,則
會引發系統異常。
而程序在啟動的時候,并不會一次性加載程序所要用到的 class 文件,而是根據
程序的需要,通過 Java 的類加載制(ClassLoader)來動態加載某個 class 文件
到內存當的,從而只有 class 文件被載入到了內存之后,才能被其它 class 文件
所引用。所以 ClassLoader 就是用來動態加載 class 件到內存當中用的。
雙親機制
類的加載就是虛擬機通過一個類的全限定名來獲取描述此類的二進制字節流,而
完成這個加載動作的就是類加載器。
類和類加載器息息相關,判定兩個類是否相等,只有在這兩個類被同一個類加載
器加載的情況下才有意義,否則即便是兩個類來自同一個 Class 文件,被不同類
加載器加載,它們也是不相等的。
注:這里的相等性保函 Class 對象的 equals()方法、isAssignableFrom()方法、
isInstance()方法的返回結果以及 Instance 關鍵字對對象所屬關系的判定結果等。
類加載器可以分為三類:啟動類加載器(Bootstrap ClassLoader):負責加載<JAVA_HOME>\lib
目錄下或者被-Xbootclasspath 參數所指定的路徑的,并且是被虛擬機所
識別的庫到內存中。
擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext
目錄下或者被 java.ext.dirs 系統變量所指定的路徑的所有類庫到內存中。
應用類加載器(Application ClassLoader):負責加載用戶類路徑上的指
定類庫,如果應用程序中沒有實現自己的類加載器,一般就是這個類加載
器去加載應用程序中的類庫。
1、原理介紹
ClassLoader 使用的是雙親委托模型來搜索類的,每個 ClassLoader 實例都有一
個父類加載器的引用(不是繼承的關系,是一個包含的關系),虛擬機內置的類
加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它
lassLoader 實例的的父類加載器。
當一個 ClassLoader 實例需要加載某個類時,它會在試圖搜索某個類之前,先把
這個任務委托給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂
層的類加載器 Bootstrap ClassLoader 試圖加載,如果沒加載到,則把任務轉交
給 Extension ClassLoader 試圖加載,如果也沒加載到,則轉交給 App ClassLoader
進行加載,如果它也沒有加載得到的話,則返回給委托的發起者,由它到指定的
文件系統或網絡等待 URL 中加載該類。
如果它們都沒有加載到這個類時,則拋出 ClassNotFoundException 異常。否則
將這個找到的類生成一個類的定義,將它加載到內存當中,最后返回這個類在內
存中的 Class 實例對象。
類加載機制:類的加載指的是將類的.class 文件中的二進制數據讀入到內存中,將其放在運行
時數據區的方法去內,然后在堆區創建一個 java.lang.Class 對象,用來封裝在方
法區內的數據結構。類的加載最終是在堆區內的 Class 對象,Class 對象封裝了
類在方法區內的數據結構,并且向 Java 程序員提供了訪問方法區內的數據結構
的接口。
類加載有三種方式:
1)命令行啟動應用時候由 JVM 初始化加載
2)通過 Class.forName()方法動態加載
3)通過 ClassLoader.loadClass()方法動態加載
這么多類加載器,那么當類在加載的時候會使用哪個加載器呢?
這個時候就要提到類加載器的雙親委派模型,流程圖如下所示:
雙親委派模型的整個工作流程非常的簡單,如下所示:
如果一個類加載器收到了加載類的請求,它不會自己立去加載類,它會先去請求
父類加載器,每個層次的類加器都是如此。層層傳遞,直到傳遞到最高層的類加
載器只有當 父類加載器反饋自己無法加載這個類,才會有當子類加載器去加載
該類。
2、為什么要使用雙親委托這種模型呢?
因為這樣可以避免重復加載,當父親已經加載了該類的時候,就沒有必要讓子
ClassLoader 再加載一次。
考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時
使用自定義的 String 來動態替代 java 核心 api 中定義的類型,這樣會存在非常
大的安全隱患,而雙親委托的方式,就可以避免這種情況,因為 String 已經在啟動時就被引導類加載器(BootstrcpClassLoader)加載,所以用戶自定義的
ClassLoader 永遠也無法加載一個自己寫的 String,除非你改變 JDK 中
ClassLoader 搜索類的默認算法。
3、但是 JVM 在搜索類的時候,又是如何判定兩個 class 是相同的呢?
JVM 在判定兩個 class 是否相同時,不僅要判斷兩個類名否相同,而且要判斷是
否由同一個類加載器實例加載的。
只有兩者同時滿足的情況下,JVM 才認為這兩個 class 是相同的。就算兩個 class
是同一份 class 字節碼,如果被兩個不同的 ClassLoader 實例所加載,JVM 也會
認為它們是兩個不同 class。
比如網絡上的一個 Java 類 org.classloader.simple.NetClassLoaderSimple,javac
編譯之后生成字節碼文件 NetClasLoaderSimple.class,ClassLoaderA 和
ClassLoaderB 這個類加載器并讀取了 NetClassLoaderSimple.class 文件并分別定
義出了 java.lang.Class 實例來表示這個類,對 JVM 來說,它們是兩個不同的實
例對象,但它們確實是一份字節碼文件,如果試圖將這個 Class 實例生成具體的
對象進行轉換時,就會拋運行時異常 java.lang.ClassCastException,提示這是兩
個不同的類型。
Android 類加載器
對于 Android 而言,最終的 apk 文件包含的是 dex 類型的文件,dex 文件是將
class 文件重新打包,打包的規則又不是簡單地壓縮,而是完全對 class 文件內部
的各種函數表進行優化,產生一個新的文件,即 dex 文件。因此加載某種特殊的
Class 文件就需要特殊的類加載器 DexClassLoader。可以動態加載 Jar 通過 URLClassLoader
1.ClassLoader 隔離問題:JVM 識別一個類是由 ClassLoaderid + PackageName
+ ClassName。
2.加載不同 Jar 包中的公共類:
?
讓父 ClassLoader 加載公共的 Jar,子 ClassLoade 加載包含公共 Jar 的 Jar,
此時子 ClassLoader 在加載 Jar 的時候會先去父 ClassLoader 中找。(只適
用 Java)
?
重寫加載包含公共 Jar 的 Jar 的 ClassLoader,在 loClass 中找到已經加載
過公共 Jar 的 ClassLoader,是把父 ClassLoader 替換掉。(只適用 Java)
?
在生成包含公共 Jar 的 Jar 時候把公共 Jar 去掉。
5、JVM 跟 Art、Dalvik 對比?
6、GC 收集器簡介?以及它的內存劃分怎么樣的?
(1)簡介:
Garbage-First(G1,垃圾優先)收集器是服務類型的收集器,目標是多處理器
機器、大內存機器。它高度符合垃圾收集暫停時間的目標,同時實現高吞吐量。
Oracle JDK 7 update 4 以及更新發布版完全支持 G1 垃圾收集器
(2)G1 的內存劃分方式:它是將堆內存被劃分為多個大小相等的 heap 區,每個 heap 區都是邏輯上連續
的一段內存(virtual memory). 其中一部分區域被當成老一代收集器相同的角色
(eden, survivor, old), 但每個角色的區域個數都不是固定的。這在內存使用上提
供了更多的靈活性
7、Java 的虛擬機 JVM 的兩個內存:棧內存和堆內存的區別是什么?
Java 把內存劃分成兩種:一種是棧內存,一種是堆內存。兩者的區別是:
1)棧內存:在函數中定義的一些基本類型的變量和對象的引用變量都在函數的
棧內存中分配。 當在一段代碼塊定義一個變量時,Java 就在棧中為這個變量分
配內存空間,當超過變量的作用域后,Java 會自動釋放掉為該變量所分配的內存
空間,該內存空間可以立即被另作他用。
2)堆內存:堆內存用來存放由 new 創建的對象和數組。在堆中分配的內存,由
Java 虛擬機的自動垃圾回收器來管理。
8、JVM 調優的常見命令行工具有哪些?JVM 常見的調優參數有哪些?
(1)JVM 調優的常見命令工具包括:
1)jps 命令用于查詢正在運行的 JVM 進程,
2)jstat 可以實時顯示本地或遠程 JVM 進程中類裝載、內存、垃圾收集、JIT 編
譯等數據
3)jinfo 用于查詢當前運行這的 JVM 屬性和參數的值。
4)jmap 用于顯示當前 Java 堆和永久代的詳細信息5)jhat 用于分析使用 jmap 生成的 dump 文件,是 JDK 自帶的工具
6)jstack 用于生成當前 JVM 的所有線程快照,線程快照是虛擬機每一條線程正
在執行的方法,目的是定位線程出現長時間停頓的原因。
(2)JVM 常見的調優參數包括:
-Xmx
指定 java 程序的最大堆內存, 使用 java -Xmx5000M -version 判斷當前系統
能分配的最大堆內存
-Xms
指定最小堆內存, 通常設置成跟最大堆內存一樣,減少 GC
-Xmn
設置年輕代大小。整個堆大小=年輕代大小 + 年老代大小。所以增大年輕
代后,將會減小年老代大小。此值對系統性能影響較大,Sun 官方推薦配置為
個堆的 3/8。
-Xss
指定線程的最大棧空間, 此參數決定了 java 函數調用的深度, 值越大調用深
度越深, 若值太小則容易出棧溢出錯誤(StackOverflowError)
-XX:PermSize指定方法區(永久區)的初始值,默認是物理內存的 1/64, 在 Java8 永久區移
除, 代之的是元數據區, 由-XX:
------分隔線----------------------------
鋒哥公眾號


鋒哥微信號


日本色在线