【建議收藏】2020年中高級Android大廠面試祕籍,爲你保駕護航金三銀四,直通大廠(Java篇)

前言

成爲一名優秀的Android開發,須要一份完備的知識體系,在這裏,讓咱們一塊兒成長爲本身所想的那樣~。

🔥 A awesome android expert interview questions and answers(continuous updating ...)html

從幾十份頂級面試倉庫和300多篇高質量面經中總結出一份全面成體系化的Android高級面試題集。java

歡迎來到2020年中高級Android大廠面試祕籍,爲你保駕護航金三銀四,直通大廠的Java。python

Java面試題

Java基礎

1、面向對象 (⭐⭐⭐)

一、談談對java多態的理解?

多態是指父類的某個方法被子類重寫時,能夠產生本身的功能行爲,同一個操做做用於不一樣對象,能夠有不一樣的解釋,產生不一樣的執行結果。android

多態的三個必要條件:git

  • 繼承父類。
  • 重寫父類的方法。
  • 父類的引用指向子類對象。

什麼是多態程序員

面向對象的三大特性:封裝、繼承、多態。從必定角度來看,封裝和繼承幾乎都是爲多態而準備的。這是咱們最後一個概念,也是最重要的知識點。github

多態的定義:指容許不一樣類的對象對同一消息作出響應。即同一消息能夠根據發送對象的不一樣而採用多種不一樣的行爲方式。(發送消息就是函數調用)面試

實現多態的技術稱爲:動態綁定(dynamic binding),是指在執行期間判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。算法

多態的做用:消除類型之間的耦合關係。數據庫

現實中,關於多態的例子不勝枚舉。比方說按下 F1 鍵這個動做,若是當前在 Flash 界面下彈出的就是 AS 3 的幫助文檔;若是當前在 Word 下彈出的就是 Word 幫助;在 Windows 下彈出的就是 Windows 幫助和支持。同一個事件發生在不一樣的對象上會產生不一樣的結果。

多態的好處:

1.可替換性(substitutability)。多態對已存在代碼具備可替換性。例如,多態對圓Circle類工做,對其餘任何圓形幾何體,如圓環,也一樣工做。

2.可擴充性(extensibility)。多態對代碼具備可擴充性。增長新的子類不影響已存在類的多態性、繼承性,以及其餘特性的運行和操做。實際上新加子類更容易得到多態功能。例如,在實現了圓錐、半圓錐以及半球體的多態基礎上,很容易增添球體類的多態性。

3.接口性(interface-ability)。多態是超類經過方法簽名,向子類提供了一個共同接口,由子類來完善或者覆蓋它而實現的。

4.靈活性(flexibility)。它在應用中體現了靈活多樣的操做,提升了使用效率。

5.簡化性(simplicity)。多態簡化對應用軟件的代碼編寫和修改過程,尤爲在處理大量對象的運算和操做時,這個特色尤其突出和重要。

Java中多態的實現方式:接口實現,繼承父類進行方法重寫,同一個類中進行方法重載。

二、你所知道的設計模式有哪些?

答:Java 中通常認爲有23種設計模式,咱們不須要全部的都會,可是其中經常使用的種設計模式應該去掌握。下面列出了全部的設計模式。要掌握的設計模式我單獨列出來了,固然能掌握的越多越好。

整體來講設計模式分爲三大類:

建立型模式,共五種:

工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。

結構型模式,共七種:

適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。

行爲型模式,共十一種:

策略模式、模板方法模式、觀者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。

具體可見個人設計模式總結筆記

三、經過靜態內部類實現單例模式有哪些優勢?

  1. 不用 synchronized ,節省時間。
  2. 調用 getInstance() 的時候纔會建立對象,不調用不建立,節省空間,這有點像傳說中的懶漢式。

四、靜態代理和動態代理的區別,什麼場景使用?

靜態代理與動態代理的區別在於代理類生成的時間不一樣,即根據程序運行前代理類是否已經存在,能夠將代理分爲靜態代理和動態代理。若是須要對多個類進行代理,而且代理的功能都是同樣的,用靜態代理重複編寫代理類就很是的麻煩,能夠用動態代理動態的生成代理類。

// 爲目標對象生成代理對象
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使用了動態代理極大地提高了擴展性和可維護性。

五、簡單工廠、工廠方法、抽象工廠、Builder模式的區別?

  • 簡單工廠模式:一個工廠方法建立不一樣類型的對象。
  • 工廠方法模式:一個具體的工廠類負責建立一個具體對象類型。
  • 抽象工廠模式:一個具體的工廠類負責建立一系列相關的對象。
  • Builder模式:對象的構建與表示分離,它更注重對象的建立過程。

六、裝飾模式和代理模式有哪些區別 ?與橋接模式相比呢?

  • 一、裝飾模式是以客戶端透明的方式擴展對象的功能,是繼承關係的一個替代方案;而代理模式則是給一個對象提供一個代理對象,並由代理對象來控制對原有對象的引用。
  • 二、裝飾模式應該爲所裝飾的對象加強功能;代理模式對代理的對象施加控制,但不對對象自己的功能進行增長。
  • 三、橋接模式的做用於代理、裝飾大相徑庭,它主要是爲了應對某個類族有多個變化維度致使子類類型急劇增多的場景。經過橋接模式將多個變化維度隔離開,使得它們能夠獨立地變化,最後經過組合使它們應對多維變化,減小子類的數量和複雜度。

七、外觀模式和中介模式的區別?

外觀模式重點是對外封裝統一的高層接口,便於用戶使用;而中介模式則是避免多個互相協做的對象直接引用,它們之間的交互經過一箇中介對象進行,從而使得它們耦合鬆散,可以易於應對變化。

八、策略模式和狀態模式的區別?

雖然二者的類型結構是一致的,可是它們的本質倒是不同的。策略模式重在整個算法的替換,也就是策略的替換,而狀態模式則是經過狀態來改變行爲。

九、適配器模式,裝飾者模式,外觀模式的異同?

這三個模式的相同之處是,它們都做用於用戶與真實被使用的類或系統之間,做一箇中間層,起到了讓用戶間接地調用真實的類的做用。它們的不一樣以外在於,如上所述的應用場合不一樣和本質的思想不一樣。

代理與外觀的主要區別在於,代理對象表明一個單一對象,而外觀對象表明一個子系統,代理的客戶對象沒法直接訪問對象,由代理提供單獨的目標對象的訪問,而一般外觀對象提供對子系統各元件功能的簡化的共同層次的調用接口。代理是一種原來對象的表明,其它須要與這個對象打交道的操做都是和這個表明交涉的。而適配器則不須要虛構出一個表明者,只須要爲應付特定使用目的,將原來的類進行一些組合。

外觀與適配器都是對現存系統的封裝。外觀定義的新的接口,而適配器則是複用一個原有的接口,適配器是使兩個已有的接口協同工做,而外觀則是爲現存系統提供一個更爲方便的訪問接口。若是硬要說外觀是適配,那麼適配器有用來適配對象的,而外觀是用來適配整個子系統的。也就是說,外觀所針對的對象的粒度更大。

代理模式提供與真實的類一致的接口,意在用代理類來處理真實的類,實現一些特定的服務或真實類的部分功能,Facade(外觀)模式注重簡化接口,Adapter(適配器)模式注重轉換接口。

十、代碼的壞味道:

一、代碼重複:

代碼重複幾乎是最多見的異味了。他也是Refactoring 的主要目標之一。代碼重複每每來自於copy-and-paste 的編程風格。

二、方法過長:

一個方法應當具備自我獨立的意圖,不要把幾個意圖放在一塊兒。

三、類提供的功能太多:

把太多的責任交給了一個類,一個類應該僅提供一個單一的功能。

四、數據泥團:

某些數據一般像孩子同樣成羣玩耍:一塊兒出如今不少類的成員變量中,一塊兒出如今許多方法的參數中…..,這些數據或許應該本身獨立造成對象。 好比以單例的形式對外提供本身的實例。

五、冗贅類:

一個幹活很少的類。類的維護須要額外的開銷,若是一個類承擔了太少的責任,應當消除它。

六、須要太多註釋:

常常以爲要寫不少註釋表示你的代碼難以理解。若是這種感受太多,表示你須要Refactoring。

十一、是否能從Android中舉幾個例子說說用到了什麼設計模式 ?

AlertDialog、Notification源碼中使用了Bulider(建造者)模式完成參數的初始化:

在AlertDialog的Builder模式中並無看到Direcotr角色的出現,其實在不少場景中,Android並無徹底按照GOF的經典設計模式來實現,而是作了一些修改,使得這個模式更易於使用。這個的AlertDialog.Builder同時扮演了上下文中提到的builder、ConcreteBuilder、Director的角色,簡化了Builder模式的設計。當模塊比較穩定,不存在一些變化時,能夠在經典模式實現的基礎上作出一些精簡,而不是照搬GOF上的經典實現,更不要生搬硬套,使程序失去架構之美。

定義:將一個複雜對象的構建與它的表示分離,使得一樣的構建過程能夠建立不一樣的表示。即將配置從目標類中隔離出來,避免過多的setter方法。

優勢:

  • 一、良好的封裝性,使用建造者模式能夠使客戶端沒必要知道產品內部組成的細節。
  • 二、建造者獨立,容易擴展。

缺點:

  • 會產生多餘的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外觀模式:

要求一個子系統的外部與其內部的通訊必須經過一個統一的對象進行,門面模式提供一個高層次的接口,使得子系統更易於使用。

使用場景:

  • 爲一個複雜子系統提供一個簡單接口。

優勢:

  • 對客戶程序隱藏子系統細節,於是減小了客戶對於子系統的耦合,可以擁抱變化。
  • 外觀類對子系統的接口封裝,使得系統更易用使用。

缺點:

  • 外觀類接口膨脹。
  • 外觀類沒有遵循開閉原則,當業務出現變動時,可能須要直接修改外觀類。

2、集合框架 (⭐⭐⭐)

一、集合框架,list,map,set都有哪些具體的實現類,區別都是什麼?

Java集合裏使用接口來定義功能,是一套完善的繼承體系。Iterator是全部集合的總接口,其餘全部接口都繼承於它,該接口定義了集合的遍歷操做,Collection接口繼承於Iterator,是集合的次級接口(Map獨立存在),定義了集合的一些通用操做。

Java集合的類結構圖以下所示:

image

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:其實咱們工做的過程當中對集合的使用是很頻繁的,稍注意並總結積累一下,在面試的時候應該會回答的很輕鬆)

二、set集合從原理上如何保證不重複?

1)在往set中添加元素時,若是指定元素不存在,則添加成功。

2)具體來說:當向HashSet中添加元素的時候,首先計算元素的hashcode值,而後用這個(元素的hashcode)%(HashMap集合的大小)+1計算出這個元素的存儲位置,若是這個位置爲空,就將元素添加進去;若是不爲空,則用equals方法比較元素是否相等,相等就不添加,不然找一個空位添加。

三、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之前)

四、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 衝突採用的頭插法造成的鏈表,在併發條件下會造成循環鏈表,一旦有查詢落到了這個鏈表上,當獲取不到值時就會死循環。

五、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的相鄰位置插入。

假設數據量都在千級之內的狀況下:

一、若是key的類型已經肯定爲int類型,那麼使用SparseArray,由於它避免了自動裝箱的過程,若是key爲long類型,它還提供了一個LongSparseArray來確保key爲long類型時的使用

二、若是key類型爲其它的類型,則使用ArrayMap。

3、反射 (⭐⭐⭐)

一、說說你對Java反射的理解?

答:Java 中的反射首先是可以獲取到Java中要反射類的字節碼, 獲取字節碼有三種方法:

1.Class.forName(className)

2.類名.class

3.this.getClass()。

而後將字節碼中的方法,變量,構造函數等映射成相應的Method、Filed、Constructor等類,這些類提供了豐富的方法能夠被咱們所使用。

深刻解析Java反射(1) - 基礎

Java基礎之—反射(很是重要)

4、泛型 (⭐⭐)

一、簡單介紹一下java中的泛型,泛型擦除以及相關的概念,解析與分派?

泛型是Java SE1.5的新特性,泛型的本質是參數化類型,也就是說所操的數據類型被指定爲一個參數。這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類、泛型接口、泛型方法。 Java語言引入泛型的好處是安全簡單。

在Java SE 1.5以前,沒有泛型的狀況的下,經過對類型Object的引用來實現參數的「任意化」,「任意化」帶來的缺點是要作顯式的強制類型轉換,而這種轉換是要求開發者實際參數類型能夠預知的狀況下進行的。對於強制類型換錯誤的狀況,編譯器可能不提示錯誤,在運行的時候出現異常,這是一個安全隱患。

泛型的好處是在編譯的時候檢查類型安全,而且全部的轉換都是自動和隱式的,提升代碼的重用率。

一、泛型的類型參數只能是類類型(包括自定義類),不是簡單類型。

二、同一種泛型能夠對應多個版本(由於參數類型是不確的),不一樣版本的泛型類實例是不兼容的。

三、泛型的類型參數能夠有多個。

四、泛型的參數類型能夠使用extends語句,例如。習慣上稱爲「有界類型」。

五、泛型的參數類型還能夠是通配符類型。例如Class<?> classType = Class.forName("java.lang.String");

泛型擦除以及相關的概念

泛型信息只存在代碼編譯階段,在進入JVM以前,與泛型關的信息都會被擦除掉。

在類型擦除的時候,若是泛型類裏的類型參數沒有指定上限,則會被轉成Object類型,若是指定了上限,則會被傳轉換成對應的類型上限。

Java中的泛型基本上都是在編譯器這個層次來實現的。生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候擦除掉。這個過程就稱爲類型擦除。

類型擦除引發的問題及解決方法:

一、先檢查,在編譯,以及檢查編譯的對象和引用傳遞的題

二、自動類型轉換

三、類型擦除與多態的衝突和解決方法

四、泛型類型變量不能是基本數據類型

五、運行時類型查詢

六、異常中使用泛型的問題

七、數組(這個不屬於類型擦除引發的問題)

九、類型擦除後的衝突

十、泛型在靜態方法和靜態類中的問題

5、註解 (⭐⭐)

一、說說你對Java註解的理解?

註解至關於一種標記,在程序中加了註解就等於爲程序打上了某種標記。程序能夠利用ava的反射機制來了解你的類及各類元素上有無何種標記,針對不一樣的標記,就去作相應的事件。標記能夠加在包,類,字段,方法,方法的參數以及局部變量上。

6、其它 (⭐⭐)

一、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不是字符數。

二、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虛擬機堆內存大小的限制

三、Java的匿名內部類有哪些限制?

考察匿名內部類的概念和用法(初級)
  • 匿名內部類的名字:沒有人類認知意義上的名字
  • 只能繼承一個父類或實現一個接口
  • 包名.OuterClass1,表示定位的第一個匿名內部類。外部類加N,N是匿名內部類的順序。
考察語言規範以及語言的橫向對比等(中級)

匿名內部類的繼承結構:Java中的匿名內部類不能夠繼承,只有內部類才能夠有實現繼承、實現接口的特性。而Kotlin是的匿名內部類是支持繼承的,如

val runnableFoo = object: Foo(),Runnable { 
        override fun run() { 
        
        } 
}
複製代碼
做爲考察內存泄漏的切入點(高級)

匿名內部類的構造方法(深刻源碼字節碼探索語言本質的能力):

  • 匿名內部類會默認持有外部類的引用,可能會致使內存泄漏。
  • 由編譯器生成的。

其參數列表包括

  • 外部對象(定義在非靜態域內)
  • 父類的外部對象(父類非靜態)
  • 父類的構造方法參數(父類有構造方法且參數列表不爲空)
  • 外部捕獲的變量(方法體內有引用外部final變量)

Lambda轉換(SAM類型,僅支持單一接口類型):

若是CallBack是一個interface,不是抽象類,則能夠轉換爲Lambda表達式。

CallBack callBack = () -> { 
        ... 
};
複製代碼
總結
  • 沒有人類認知意義上的名字。
  • 只能繼承一個父類或實現一個接口。
  • 父類是非靜態的類型,則需父類外部實例來初始化。
  • 若是定義在非靜態做用域內,會引用外部類實例。
  • 只能捕獲外部做用域內的final變量。
  • 建立時只有單一方法的接口能夠用Lambda轉換。
技巧點撥

關注語言版本的變化:

  • 體現對技術的熱情
  • 體現好學的品質
  • 顯得專業

四、Java中對異常是如何進行分類的?

異常總體分類:

Java異常結構中定義有Throwable類。 Exception和Error爲其子類。

Error是程序沒法處理的錯誤,好比OutOfMemoryError、StackOverflowError。這些異常發生時, Java虛擬機(JVM)通常會選擇線程終止。

Exception是程序自己能夠處理的異常,這種異常分兩大類運行時異常和非運行時異常,程序中應當儘量去處理這些異常。

運行時異常都是RuntimeException類及其子類異常,如NullPointerException、IndexOutOfBoundsException等, 這些異常是不檢查異常,程序中能夠選擇捕獲處理,也能夠不處理。這些異常通常是由程序邏輯錯誤引發的, 程序應該從邏輯角度儘量避免這類異常的發生。

異常處理的兩個基本原則:

一、儘可能不要捕獲相似 Exception 這樣的通用異常,而是應該捕獲特定異常。

二、不要生吞異常。

NoClassDefFoundError 和 ClassNotFoundException 有什麼區別?

ClassNotFoundException的產生緣由主要是: Java支持使用反射方式在運行時動態加載類,例如使用Class.forName方法來動態地加載類時,能夠將類名做爲參數傳遞給上述方法從而將指定類加載到JVM內存中,若是這個類在類路徑中沒有被找到,那麼此時就會在運行時拋出ClassNotFoundException異常。 解決該問題須要確保所需的類連同它依賴的包存在於類路徑中,常見問題在於類名書寫錯誤。 另外還有一個致使ClassNotFoundException的緣由就是:當一個類已經某個類加載器加載到內存中了,此時另外一個類加載器又嘗試着動態地從同一個包中加載這個類。經過控制動態類加載過程,能夠避免上述狀況發生。

NoClassDefFoundError產生的緣由在於: 若是JVM或者ClassLoader實例嘗試加載(能夠經過正常的方法調用,也多是使用new來建立新的對象)類的時候卻找不到類的定義。要查找的類在編譯的時候是存在的,運行的時候卻找不到了。這個時候就會致使NoClassDefFoundError. 形成該問題的緣由多是打包過程漏掉了部分類,或者jar包出現損壞或者篡改。解決這個問題的辦法是查找那些在開發期間存在於類路徑下但在運行期間卻不在類路徑下的類。

五、String 爲何要設計成不可變的?

String是不可變的(修改String時,不會在原有的內存地址修改,而是從新指向一個新對象),String用final修飾,不可繼承,String本質上是個final的char[]數組,因此char[]數組的內存地址不會被修改,並且String 也沒有對外暴露修改char[]數組的方法。不可變性能夠保證線程安全以及字符串串常量池的實現。

六、Java裏的冪等性瞭解嗎?

冪等性本來是數學上的一個概念,即:f(x) = f(f(x)),對同一個系統,使用一樣的條件,一次請求和重複的屢次請求對系統資源的影響是一致的。

冪等性最爲常見的應用就是電商的客戶付款,試想一下若是你在付款的時候由於網絡等各類問題失敗了,而後去重複的付了一次,是一種多麼糟糕的體驗。冪等性就是爲了解決這樣的問題。

實現冪等性能夠使用Token機制。

核心思想是爲每一次操做生成一個惟一性的憑證,也就是token。一個token在操做的每個階段只有一次執行權,一旦執行成功則保存執行結果。對重複的請求,返回同一個結果。

例如:電商平臺上的訂單id就是最適合的token。當用戶下單時,會經歷多個環節,好比生成訂單,減庫存,減優惠券等等。每個環節執行時都先檢測一下該訂單id是否已經執行過這一步驟,對未執行的請求,執行操做並緩存結果,而對已經執行過的id,則直接返回以前的執行結果,不作任何操 做。這樣能夠在最大程度上避免操做的重複執行問題,緩存起來的執行結果也能用於事務的控制等。

七、爲何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修飾的外部變量。

八、講一下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 編碼格式。

九、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 沒有本質區別,可是它去掉了線程安全的部分,有效減少了開銷,是絕大部分狀況下進行字符串拼接的首選。

十、什麼是內部類?內部類的做用。

內部類能夠有多個實例,每一個實例都有本身的狀態信息,而且與其餘外圍對象的信息相互獨立。

在單個外圍類中,可讓多個內部類以不一樣的方式實現同一個接口,或者繼承同一個類。

建立內部類對象並不依賴於外圍類對象的建立。

內部類並無使人迷惑的「is-a」關係,他就是一個獨立的實體。

內部類提供了更好的封裝,除了該外圍類,其餘類都不能訪問。。

十一、抽象類和接口區別?

共同點

  • 是上層的抽象層。
  • 都不能被實例化。
  • 都能包含抽象的方法,這些抽象的方法用於描述類具有的功能,可是不提供具體的實現。

區別:

  • 一、在抽象類中能夠寫非抽象的方法,從而避免在子類中重複書寫他們,這樣能夠提升代碼的複用性,這是抽象類的優點,接口中只能有抽象的方法。
  • 二、多繼承:一個類只能繼承一個直接父類,這個父類能夠是具體的類也但是抽象類,可是一個類能夠實現多個接口。
  • 三、抽象類能夠有默認的方法實現,接口根本不存在方法的實現。
  • 四、子類使用extends關鍵字來繼承抽象類。若是子類不是抽象類的話,它須要提供抽象類中全部聲明方法的實現。子類使用關鍵字implements來實現接口。它須要提供接口中全部聲明方法的實現。
  • 五、構造器:抽象類能夠有構造器,接口不能有構造器。
  • 六、和普通Java類的區別:除了你不能實例化抽象類以外,抽象類和普通Java類沒有任何區別,接口是徹底不一樣的類型。
  • 七、訪問修飾符:抽象方法能夠有public、protected和default修飾符,接口方法默認修飾符是public。你不能夠使用其它修飾符。
  • 八、main方法:抽象方法能夠有main方法而且咱們能夠運行它接口沒有main方法,所以咱們不能運行它。
  • 九、速度:抽象類比接口速度要快,接口是稍微有點慢的,由於它須要時間去尋找在類中實現的方法。
  • 十、添加新方法:若是你往抽象類中添加新的方法,你能夠給它提供默認的實現。所以你不須要改變你如今的代碼。若是你往接口中添加方法,那麼你必須改變實現該接口的類。

十二、接口的意義?

規範、擴展、回調。

1三、父類的靜態方法可否被子類重寫?

不能。子類繼承父類後,用相同的靜態方法和非靜態方法,這時非靜態方法覆蓋父類中的方法(即方法重寫),父類的該靜態方法被隱藏(若是對象是父類則調用該隱藏的方法),另外子類可繼承父類的靜態與非靜態方法,至於方法重載我以爲它其中一要素就是在同一類中,不能說父類中的什麼方法與子類裏的什麼方法是方法重載的體現。

1四、抽象類的意義?

爲其子類提供一個公共的類型,封裝子類中的重複內容,定義抽象方法,子類雖然有不一樣的實現 可是定義是一致的。

1五、靜態內部類、非靜態內部類的理解?

靜態內部類:只是爲了下降包的深度,方便類的使用,靜態內部類適用於包含在類當中,但又不依賴與外在的類,不用使用外在類的非靜態屬性和方法,只是爲了方便管理類結構而定義。在建立靜態內部類的時候,不須要外部類對象的引用。

非靜態內部類:持有外部類的引用,能夠自由使用外部類的全部變量和方法。

1六、爲何複寫equals方法的同時須要複寫hashcode方法,前者相同後者是否相同,反過來呢?爲何?

要考慮到相似HashMap、HashTable、HashSet的這種散列的數據類型的運用,當咱們重寫equals時,是爲了用自身的方式去判斷兩個自定義對象是否相等,然而若是此時恰好須要咱們用自定義的對象去充當hashmap的鍵值使用時,就會出現咱們認爲的同一對象,卻由於hash值不一樣而致使hashmap中存了兩個對象,從而才須要進行hashcode方法的覆蓋。

1七、equals 和 hashcode 的關係?

hashcode和equals的約定關係以下:

  • 一、若是兩個對象相等,那麼他們必定有相同的哈希值(hashcode)。

  • 二、若是兩個對象的哈希值相等,那麼這兩個對象有可能相等也有可能不相等。(須要再經過equals來判斷)

1八、java爲何跨平臺?

由於Java程序編譯以後的代碼不是能被硬件系統直接運行的代碼,而是一種「中間碼」——字節碼。而後不一樣的硬件平臺上安裝有不一樣的Java虛擬機(JVM),由JVM來把字節碼再「翻譯」成所對應的硬件平臺可以執行的代碼。所以對於Java編程者來講,不須要考慮硬件平臺是什麼。因此Java能夠跨平臺。

1九、浮點數的精準計算

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 更加輕量、更加可靠。

2一、靜態內部類的設計意圖

靜態內部類與非靜態內部類之間存在一個最大的區別:非靜態內部類在編譯完成以後會隱含地保存着一個引用,該引用是指向建立它的外圍內,可是靜態內部類卻沒有。

沒有這個引用就意味着:

它的建立是不須要依賴於外圍類的。 它不能使用任何外圍類的非static成員變量和方法。

2二、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)

垃圾回收器對該對象的所佔用的內存空間進行回收或者再分配了,則該對象完全消失了,稱之爲「對象空間從新分配階段。

2三、靜態屬性和靜態方法是否能夠被繼承?是否能夠被重寫?以及緣由?

結論:java中靜態屬性和靜態方法能夠被繼承,可是不能夠被重寫而是被隱藏。

緣由:

1). 靜態方法和屬性是屬於類的,調用的時候直接經過類名.方法名完成,不須要繼承機制便可以調用。若是子類裏面定義了靜態方法和屬性,那麼這時候父類的靜態方法或屬性稱之爲"隱藏"。若是你想要調用父類的靜態方法和屬性,直接經過父類名.方法或變量名完成,至因而否繼承一說,子類是有繼承靜態方法和屬性,可是跟實例方法和屬性不太同樣,存在"隱藏"的這種狀況。

2). 多態之因此可以實現依賴於繼承、接口和重寫、重載(繼承和重寫最爲關鍵)。有了繼承和重寫就能夠實現父類的引用指向不一樣子類的對象。重寫的功能是:"重寫"後子類的優先級要高於父類的優先級,可是「隱藏」是沒有這個優先級之分的。

3). 靜態屬性、靜態方法和非靜態的屬性均可以被繼承和隱藏而不能被重寫,所以不能實現多態,不能實現父類的引用能夠指向不一樣子類的對象。非靜態方法能夠被繼承和重寫,所以能夠實現多態。

2四、object類的equal 和hashcode 方法重寫,爲何?

在Java API文檔中關於hashCode方法有如下幾點規定(原文來自java深刻解析一書):

一、在java應用程序執行期間,若是在equals方法比較中所用的信息沒有被修改,那麼在同一個對象上屢次調用hashCode方法時必須一致地返回相同的整數。若是屢次執行同一個應用時,不要求該整數必須相同。

二、若是兩個對象經過調用equals方法是相等的,那麼這兩個對象調用hashCode方法必須返回相同的整數。

三、若是兩個對象經過調用equals方法是不相等的,不要求這兩個對象調用hashCode方法必須返回不一樣的整數。可是程序員應該意識到對不一樣的對象產生不一樣的hash值能夠提供哈希表的性能。

2五、java中==和equals和hashCode的區別?

默認狀況下也就是從超類Object繼承而來的equals方法與‘==’是徹底等價的,比較的都是對象的內存地址,但咱們能夠重寫equals方法,使其按照咱們的需求的方式進行比較,如String類重寫了equals方法,使其比較的是字符的序列,而再也不是內存地址。在java的集合中,判斷兩個對象是否相等的規則是:

1.判斷兩個對象的hashCode是否相等。
  2.判斷兩個對象用equals運算是否相等。
複製代碼

2六、Java的四種引用及使用場景?

  • 強引用(FinalReference):在內存不足時不會被回收。日常用的最多的對象,如新建立的對象。
  • 軟引用(SoftReference):在內存不足時會被回收。用於實現內存敏感的高速緩存。
  • 弱引用(WeakReferenc):只要GC回收器發現了它,就會將之回收。用於Map數據結構中,引用佔用內存空間較大的對象。
  • 虛引用(PhantomReference):在回收以前,會被放入ReferenceQueue,JVM不會自動將該referent字段值設置成null。其它引用被JVM回收以後纔會被放入ReferenceQueue中。用於實現一個對象被回收以前作一些清理工做。

2七、類的加載過程,Person person = new Person();爲例進行說明。

1).由於new用到了Person.class,因此會先找到Person.class文件,並加載到內存中;

2).執行該類中的static代碼塊,若是有的話,給Person.class類進行初始化;

3).在堆內存中開闢空間分配內存地址;

4).在堆內存中創建對象的特有屬性,並進行默認初始化;

5).對屬性進行顯示初始化;

6).對對象進行構造代碼塊初始化;

7).對對象進行與之對應的構造函數進行初始化;

8).將內存地址付給棧內存中的p變量。

2八、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對象。

2九、在重寫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、深拷貝和淺拷貝的區別

3一、Integer類對int的優化

Java併發

1、線程池相關 (⭐⭐⭐)

一、什麼是線程池,如何使用?爲何要使用線程池?

答:線程池就是事先將多個線程對象放到一個容器中,使用的時候就不用new線程而是直接去池中拿線程便可,節 省了開闢子線程的時間,提升了代碼執行效率。

二、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>());
}
複製代碼

可緩存線程池:

一、線程數無限制。 二、有空閒線程則複用空閒線程,若無空閒線程則新建線程。 三、必定程序減小頻繁建立/銷燬線程,減小系統開銷。

第二種:newFixedThreadPool

一個固定線程數量的線程池:

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    // corePoolSize跟maximumPoolSize值同樣,同時傳入一個無界阻塞隊列
    // 該線程池的線程會維持在指定線程數,不會進行回收
    return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory);
}
複製代碼

定長線程池:

一、可控制線程最大併發數(同時執行的線程數)。 二、超出的線程會在隊列中等待。

第三種:newSingleThreadExecutor

能夠理解爲線程數量爲1的FixedThreadPool:

public static ExecutorService newSingleThreadExecutor() {
    // 線程池中只有一個線程進行任務執行,其餘的都放入阻塞隊列
    // 外面包裝的FinalizableDelegatedExecutorService類實現了finalize方法,在JVM垃圾回收的時候會關閉線程池
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
複製代碼

單線程化的線程池:

一、有且僅有一個工做線程執行任務。 二、全部任務按照指定順序執行,即遵循隊列的入隊出隊規則。

第四種:newScheduledThreadPool。

支持定時以指定週期循環執行任務:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
複製代碼

注意:前三種線程池是ThreadPoolExecutor不一樣配置的實例,最後一種是ScheduledThreadPoolExecutor的實例。

三、線程池原理?

從數據結構的角度來看,線程池主要使用了阻塞隊列(BlockingQueue)和HashSet集合構成。 從任務提交的流程角度來看,對於使用線程池的外部來講,線程池的機制是這樣的:

一、若是正在運行的線程數 < coreSize,立刻建立核心線程執行該task,不排隊等待;
二、若是正在運行的線程數 >= coreSize,把該task放入阻塞隊列;
三、若是隊列已滿 && 正在運行的線程數 < maximumPoolSize,建立新的非核心線程執行該task;
四、若是隊列已滿 && 正在運行的線程數 >= maximumPoolSize,線程池調用handler的reject方法拒絕本次提交。
複製代碼

理解記憶:1-2-3-4對應(核心線程->阻塞隊列->非核心線程->handler拒絕提交)。

線程池的線程複用:

這裏就須要深刻到源碼addWorker():它是建立新線程的關鍵,也是線程複用的關鍵入口。最終會執行到runWoker,它取任務有兩個方式:

  • firstTask:這是指定的第一個runnable可執行任務,它會在Woker這個工做線程中運行執行任務run。而且置空表示這個任務已經被執行。
  • getTask():這首先是一個死循環過程,工做線程循環直到可以取出Runnable對象或超時返回,這裏的取的目標就是任務隊列workQueue,對應剛纔入隊的操做,有入有出。

其實就是任務在並不僅執行建立時指定的firstTask第一任務,還會從任務隊列的中經過getTask()方法本身主動去取任務執行,並且是有/無時間限定的阻塞等待,保證線程的存活。

信號量

semaphore 可用於進程間同步也可用於同一個進程間的線程同步。

能夠用來保證兩個或多個關鍵代碼段不被併發調用。在進入一個關鍵代碼段以前,線程必須獲取一個信號量;一旦該關鍵代碼段完成了,那麼該線程必須釋放信號量。其它想進入該關鍵代碼段的線程必須等待直到第一個線程釋放信號量。

四、線程池都有哪幾種工做隊列?

一、ArrayBlockingQueue

是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。

二、LinkedBlockingQueue

一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了這個隊列。

三、SynchronousQueue

一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。

四、PriorityBlockingQueue

一個具備優先級的無限阻塞隊列。

五、怎麼理解無界隊列和有界隊列?

有界隊列

1.初始的poolSize < corePoolSize,提交的runnable任務,會直接作爲new一個Thread的參數,立馬執行 。 2.當提交的任務數超過了corePoolSize,會將當前的runable提交到一個block queue中。 3.有界隊列滿了以後,若是poolSize < maximumPoolsize時,會嘗試new 一個Thread的進行救急處理,立馬執行對應的runnable任務。 4.若是3中也沒法處理了,就會走到第四步執行reject操做。

無界隊列

與有界隊列相比,除非系統資源耗盡,不然無界的任務隊列不存在任務入隊失敗的狀況。當有新的任務到來,系統的線程數小於corePoolSize時,則新建線程執行任務。當達到corePoolSize後,就不會繼續增長,若後續仍有新的任務加入,而沒有空閒的線程資源,則任務直接進入隊列等待。若任務建立和處理的速度差別很大,無界隊列會保持快速增加,直到耗盡系統內存。 當線程池的任務緩存隊列已滿而且線程池中的線程數目達到maximumPoolSize,若是還有任務到來就會採起任務拒絕策略。

六、多線程中的安全隊列通常經過什麼實現?

Java提供的線程安全的Queue能夠分爲阻塞隊列和非阻塞隊列,其中阻塞隊列的典型例子是BlockingQueue,非阻塞隊列的典型例子是ConcurrentLinkedQueue.

對於BlockingQueue,想要實現阻塞功能,須要調用put(e) take() 方法。而ConcurrentLinkedQueue是基於連接節點的、無界的、線程安全的非阻塞隊列。

2、Synchronized、volatile、Lock(ReentrantLock)相關 (⭐⭐⭐)

一、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,而後試圖進行降級。

二、Synchronized優化後的鎖機制簡單介紹一下,包括自旋鎖、偏向鎖、輕量級鎖、重量級鎖?

自旋鎖:

線程自旋說白了就是讓cpu在作無用功,好比:能夠執行幾回for循環,能夠執行幾條空的彙編指令,目的是佔着CPU不放,等待獲取鎖的機會。若是旋的時間過長會影響總體性能,時間太短又達不到延遲阻塞的目的。

偏向鎖

偏向鎖就是一旦線程第一次得到了監視對象,以後讓監視對象「偏向」這個線程,以後的屢次調用則能夠避免CAS操做,說白了就是置個變量,若是發現爲true則無需再走各類加鎖/解鎖流程。

輕量級鎖:

輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖競爭用的時候,偏向鎖就會升級爲輕量級鎖;

重量級鎖

重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具有Mutex(0|1)互斥的功能,它還負責實現了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責作互斥,後一個用於作線程同步。

三、談談對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實例對象的鎖了,當再須要的時候能夠繼續**「開鎖」**進去的!

這就是內置鎖的可重入性。

四、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()方法後執行。

五、synchronized關鍵字和Lock的區別你知道嗎?爲何Lock的性能好一些?

類別 synchronized Lock(底層實現主要是Volatile + CAS)
存在層次 Java的關鍵字,在jvm層面上 是一個類
鎖的釋放 一、已獲取鎖的線程執行完同步代碼,釋放鎖 二、線程執行發生異常,jvm會讓線程釋放鎖。 在finally中必須釋放鎖,否則容易形成線程死鎖。
鎖的獲取 假設A線程得到鎖,B線程等待。若是A線程阻塞,B線程會一直等待。 分狀況而定,Lock有多個鎖獲取的方式,大體就是能夠嘗試得到鎖,線程能夠不用一直等待
鎖狀態 沒法判斷 能夠判斷
鎖類型 可重入 不可中斷 非公平 可重入 可判斷 可公平(二者皆可)
性能 少許同步 大量同步

Lock(ReentrantLock)的底層實現主要是Volatile + CAS(樂觀鎖),而Synchronized是一種悲觀鎖,比較耗性能。可是在JDK1.6之後對Synchronized的鎖機制進行了優化,加入了偏向鎖、輕量級鎖、自旋鎖、重量級鎖,在併發量不大的狀況下,性能可能優於Lock機制。因此建議通常請求併發量不大的狀況下使用synchronized關鍵字。

六、volatile原理。

在《Java併發編程:核心理論》一文中,咱們已經提到可見性、有序性及原子性問題,一般狀況下咱們能夠經過Synchronized關鍵字來解決這些個問題,不過若是對Synchonized原理有了解的話,應該知道Synchronized是一個較重量級的操做,對系統的性能有比較大的影響,因此若是有其餘解決方案,咱們一般都避免使用Synchronized來解決問題。

而volatile關鍵字就是Java中提供的另外一種解決可見性有序性問題的方案。對於原子性,須要強調一點,也是你們容易誤解的一點:對volatile變量的單次讀/寫操做可保證原子性的,如long和double類型變量,可是並不能保證i++這種操做的原子性,由於本質上i++是讀、寫兩次操做。

volatile也是互斥同步的一種實現,不過它很是的輕量級。

volatile 的意義?
  • 防止CPU指令重排序

volatile有兩條關鍵的語義:

保證被volatile修飾的變量對全部線程都是可見的

禁止進行指令重排序

要理解volatile關鍵字,咱們得先從Java的線程模型開始提及。如圖所示:

image

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);
}
複製代碼

image

這段代碼啓動了10個線程,每次10次自增,按道理最終結果應該是100,可是結果並不是如此。

爲何會這樣?

仔細看一下start++,它其實並不是一個原子操做,簡單來看,它有兩步:

一、取出start的值,由於有volatile的修飾,這時候的值是正確的。

二、自增,可是自增的時候,別的線程可能已經把start加大了,這種狀況下就有可能把較小的start寫回主內存中。 因此volatile只能保證可見性,在不符合如下場景下咱們依然須要經過加鎖來保證原子性:

  • 運算結果並不依賴變量當前的值,或者只有單一線程修改變量的值。(要麼結果不依賴當前值,要麼操做是原子性的,要麼只要一個線程修改變量的值)
  • 變量不須要與其餘狀態變量共同參與不變約束 比方說咱們會在線程里加個boolean變量,來判斷線程是否中止,這種狀況就很是適合使用volatile。

咱們再來理解第二句話。

禁止進行指令重排序

什麼是指令重排序?

  • 指令重排序是指指令亂序執行,即在條件容許的狀況下直接運行當前有能力當即執行的後續指令,避開爲獲取一條指令所需數據而形成的等待,經過亂序執行的技術提供執行效率。

  • 指令重排序會在被volatile修飾的變量的賦值操做前,添加一個內存屏障,指令重排序時不能把後面的指令重排序移到內存屏障以前的位置。

七、synchronized 和 volatile 關鍵字的做用和區別。

Volatile

1)保證了不一樣線程對這個變量進行操做時的可見性即一個線程修改了某個變量的值,這新值對其餘線程來是當即可見的。

2)禁止進行指令重排序。

做用

volatile 本質是在告訴jvm當前變量在寄存器(工做內存)中的值是不肯定的,需從主存中讀取;synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其它線程被阻塞住。

區別

1.volatile 僅能使用在變量級別;synchronized則能夠使用在變量、方法、和類級別的。

2.volatile 僅能實現變量的修改可見性,並不能保證原子性;synchronized 則能夠保證變量的修改可見性和原子性。

3.volatile 不會形成線程的阻塞;synchronized 可能會形成線程的阻塞。

4.volatile 標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化。

八、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,固然在釋放的時候,也得一層一層釋放。至於公平性,在嘗試獲取鎖的時候多了一個判斷:是否有比本身申請早的線程在同步隊列中等待,如有,去等待;若沒有,才容許去搶佔。  

九、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單例。

它的字節碼以下:

image

能夠看到被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方法。反過來也是成立的,爲他們須要的鎖是不一樣的。

3、其它 (⭐⭐⭐)

一、多線程的使用場景?

使用多線程就必定效率高嗎?有時候使用多線程並非爲了提升效率,而是使得CPU能同時處理多個事件。

  • 爲了避免阻塞主線程,啓動其餘線程來作事情,好比APP中的耗時操做都不在UI線程中作。

  • 實現更快的應用程序,即主線程專門監聽用戶請求,子線程用來處理用戶請求,以得到大的吞吐量.感受這種狀況,多線程的效率未必高。這種狀況下的多線程是爲了避免必等待,能夠並行處理多條數據。好比JavaWeb的就是主線程專門監聽用戶的HTTP請求,然啓動子線程去處理用戶的HTTP請求。

  • 某種雖然優先級很低的服務,可是卻要不定時去作。好比Jvm的垃圾回收。

  • 某種任務,雖然耗時,可是不消耗CPU的操做時間,開啓個線程,效率會有顯著提升。好比讀取文件,而後處理。磁盤IO是個很耗費時間,可是不耗CPU計算的工做。因此能夠一個線程讀取數據,一個線程處理數據。確定比一個線程讀取數據,而後處理效率高。由於兩個線程的時候充分利用了CPU等待磁盤IO的空閒時間。

二、CopyOnWriteArrayList的瞭解。

Copy-On-Write 是什麼?

在計算機中就是當你想要對一塊內存進行修改時,咱們不在原有內存塊中進行寫操做,而是將內存拷貝一份,在新的內存中進行寫操做,寫完以後呢,就將指向原來內存指針指向新的內存,原來的內存就能夠被回收掉。

原理:

CopyOnWriteArrayList這是一個ArrayList的線程安全的變體,CopyOnWriteArrayList 底層實現添加的原理是先copy出一個容器(能夠簡稱副本),再往新的容器裏添加這個新的數據,最後把新的容器的引用地址賦值給了以前那個舊的的容器地址,可是在添加這個數據的期間,其餘線程若是要去讀取數據,仍然是讀取到舊的容器裏的數據。

優勢和缺點:

優勢:

1.據一致性完整,爲何?由於加鎖了,併發數據不會亂。

2.解決了像ArrayList、Vector這種集合多線程遍歷迭代問題,記住,Vector雖然線程安全,只不過是加了synchronized關鍵字,迭代問題徹底沒有解決!

缺點:

1.內存佔有問題:很明顯,兩個數組同時駐紮在內存中,若是實際應用中,數據比較多,並且比較大的狀況下,佔用內存會比較大,針對這個其實能夠用ConcurrentHashMap來代替。

2.數據一致性:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。因此若是你但願寫入的的數據,立刻能讀到,請不要使用CopyOnWrite容器。

使用場景:

一、讀多寫少(白名單,黑名單,商品類目的訪問和更新場景),爲何?由於寫的時候會複製新集合。

二、集合不大,爲何?由於寫的時候會複製新集合。

三、實時性要求不高,爲何,由於有可能會讀取到舊的集合數據。

三、ConcurrentHashMap加鎖機制是什麼,詳細說一下?

Java7 ConcurrentHashMap

ConcurrentHashMap做爲一種線程安全且高效的哈希表的解決方案,尤爲其中的"分段鎖"的方案,相比HashTable的表鎖在性能上的提高很是之大。HashTable容器在競爭激烈的併發環境下表現出效率低下的緣由,是由於全部訪問HashTable的線程都必須競爭同一把鎖,那假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不一樣數據段的數據時,線程間就不會存在鎖競爭,從而能夠有效的提升併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問。

ConcurrentHashMap 是一個 Segment 數組,Segment 經過繼承 ReentrantLock 來進行加鎖,因此每次須要加鎖的操做鎖住的是一個 segment,這樣只要保證每一個 Segment 是線程安全的,也就實現了全局的線程安全。

concurrencyLevel:並行級別、併發數、Segment 數。默認是 16,也就是說 ConcurrentHashMap 有 16 個 Segments,因此理論上,這個時候,最多能夠同時支持 16 個線程併發寫,只要它們的操做分別分佈在不一樣的 Segment 上。這個值能夠在初始化的時候設置爲其餘值,可是一旦初始化之後,它是不能夠擴容的。其中的每一個 Segment 很像 HashMap,不過它要保證線程安全,因此處理起來要麻煩些。

初始化槽: ensureSegment

ConcurrentHashMap 初始化的時候會初始化第一個槽 segment[0],對於其餘槽來講,在插入第一個值的時候進行初始化。對於併發操做使用 CAS 進行控制。

Java8 ConcurrentHashMap

拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。結構上和 Java8 的 HashMap(數組+鏈表+紅黑樹) 基本上同樣,不過它要保證線程安全性,因此在源碼上確實要複雜一些。1.8 在 1.7 的數據結構上作了大的改動,採用紅黑樹以後能夠保證查詢效率(O(logn)),甚至取消了 ReentrantLock 改成了 synchronized,這樣能夠看出在新版的 JDK 中對 synchronized 優化是很到位的。

四、線程死鎖的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");
            }
        }
    }
}
複製代碼

五、CAS介紹?

Unsafe

Unsafe是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問題,使用傳統的互斥同步可能迴避原子類更加高效。

六、進程和線程的區別?

簡而言之,一個程序至少有一個進程,一個進程至少有一個線程。

  • 一、線程的劃分尺度小於進程,使得多線程程序的併發性高。

  • 二、進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率。

  • 三、線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。

  • 四、從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分能夠同時執行。但操做系統並無將多個線程看作多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。

  • 五、進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),可是它可與同屬一個進程的其餘的線程共享進程所擁有的所有資源。

  • 六、一個線程能夠建立和撤銷另外一個線程;同一個進程中的多個線程之間能夠併發執行。

  • 七、進程有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不一樣執行路徑。線程有本身的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,因此多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。

七、什麼致使線程阻塞?

線程的阻塞

爲了解決對共享存儲區的訪問衝突,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() 方法,由於它們的功能最強大,使用也最靈活,可是這也致使了它們的效率較低,較容易出錯。實際使用中咱們應該靈活使用各類方法,以便更好地達到咱們的目的。

八、線程的生命週期

線程狀態流程圖

image

  • 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()方法可讓調用它的線程正常執行完成後,再去執行該線程後面的代碼,它具備讓線程排隊的做用。

九、樂觀鎖與悲觀鎖

悲觀鎖

老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,能夠使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

使用場景

樂觀鎖適用於寫比較少的狀況下(多讀場景),而通常多寫的場景下用悲觀鎖就比較合適。

樂觀鎖常見的兩種實現方式

一、版本號機制

通常是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加1。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,不然重試更新操做,直到更新成功。

二、CAS算法

即compare and swap(比較與交換),是一種有名的無鎖算法。CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。 通常狀況下是一個自旋操做,即不斷的重試。

樂觀鎖的缺點

一、ABA 問題

若是一個變量V初次讀取的時候是A值,而且在準備賦值的時候檢查到它仍然是A值,那咱們就能說明它的值沒有被其餘線程修改過了嗎?很明顯是不能的,由於在這段時間它的值可能被改成其餘值,而後又改回A,那CAS操做就會誤認爲它歷來沒有被修改過。這個問題被稱爲CAS操做的 "ABA"問題。

JDK 1.5 之後的 AtomicStampedReference 類必定程度上解決了這個問題,其中的 compareAndSet 方法就是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

二、自旋CAS(也就是不成功就一直循環執行直到成功)若是長時間不成功,會給CPU帶來很是大的執行開銷。

三、CAS 只對單個共享變量有效,當操做涉及跨多個共享變量時 CAS 無效。可是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行 CAS 操做.因此咱們能夠使用鎖或者利用AtomicReference類把多個共享變量合併成一個共享變量來操做。

十、run()和start()方法區別?

1.start()方法來啓動線程,真正實現了多線程運行,這時無需等待run方法體代碼執行完畢而直接繼續執行下面的代碼:

經過調用Thread類的start()方法來啓動一個線程, 這時此線程是處於就緒狀態, 並無運行。 而後經過此Thread類調用方法run()來完成其運行操做的, 這裏方法run()稱爲線程體, 它包含了要執行的這個線程的內容, Run方法運行結束, 此線程終止, 而CPU再運行其它線程,在Android中通常是主線程。

2.run()方法看成普通方法的方式調用,程序仍是要順序執行,仍是要等待run方法體執行完畢後纔可繼續執行下面的代碼:

而若是直接用Run方法, 這只是調用一個方法而已, 程序中依然只有主線程--這一個線程, 其程序執行路徑仍是隻有一條, 這樣就沒有達到寫線程的目的。

十一、多線程斷點續傳原理。

在本地下載過程當中要使用數據庫實時存儲到底存儲到文件的哪一個位置了,這樣點擊開始繼續傳遞時,才能經過HTTP的GET請求中的setRequestProperty("Range","bytes=startIndex-endIndex");方法能夠告訴服務器,數據從哪裏開始,到哪裏結束。同時在本地的文件寫入時,RandomAccessFile的seek()方法也支持在文件中的任意位置進行寫入操做。同時經過廣播或事件總線機制將子線程的進度告訴Activity的進度條。關於斷線續傳的HTTP狀態碼是206,即HttpStatus.SC_PARTIAL_CONTENT。

十二、怎麼安全中止一個線程任務?原理是什麼?線程池裏有相似機制嗎?

終止線程

一、使用violate boolean變量退出標誌,使線程正常退出,也就是當run方法完成後線程終止。(推薦)

二、使用interrupt()方法中斷線程,可是線程不必定會終止。

三、使用stop方法強行終止線程。不安全主要是:thread.stop()調用以後,建立子線程的線程就會拋出ThreadDeatherror的錯誤,而且會釋放子線程所持有的全部鎖。

終止線程池

ExecutorService線程池就提供了shutdown和shutdownNow這樣的生命週期方法來關閉線程池自身以及它擁有的全部線程。

一、shutdown關閉線程池

線程池不會馬上退出,直到添加到線程池中的任務都已經處理完成,纔會退出。

二、shutdownNow關閉線程池並中斷任務

終止等待執行的線程,並返回它們的列表。試圖中止全部正在執行的線程,試圖終止的方法是調用Thread.interrupt(),可是你們知道,若是線程中沒有sleep 、wait、Condition、定時鎖等應用, interrupt()方法是沒法中斷當前的線程的。因此,ShutdownNow()並不表明線程池就必定當即就能退出,它可能必需要等待全部正在執行的任務都執行完成了才能退出。

1三、堆內存,棧內存理解,棧如何轉換成堆?

  • 在函數中定義的一些基本類型的變量和對象的引用變量都是在函數的棧內存中分配。
  • 堆內存用於存放由new建立的對象和數組。JVM裏的「堆」(heap)特指用於存放Java對象的內存區域。因此根據這個定義,Java對象所有都在堆上。JVM的堆被同一個JVM實例中的全部Java線程共享。它一般由某種自動內存管理機制所管理,這種機制一般叫作「垃圾回收」(garbage collection,GC)。
  • 堆主要用來存放對象的,棧主要是用來執行程序的。
  • 實際上,棧中的變量指向堆內存中的變量,這就是 Java 中的指針!

1四、如何控制某個方法容許併發訪問線程的個數;

1五、多進程開發以及多進程應用場景;

1六、Java的線程模型;

1七、死鎖的概念,怎麼避免死鎖?

1八、如何保證多線程讀寫文件的安全?

1九、線程如何關閉,以及如何防止線程的內存泄漏?

20、爲何要有線程,而不是僅僅用進程?

2一、多個線程如何同時請求,返回的結果如何等待全部線程數據完成後合成一個數據?

2二、線程如何關閉?

2三、數據一致性如何保證?

2四、兩個進程同時要求寫或者讀,能不能實現?如何防止進程的同步?

2五、談談對多線程的理解並舉例說明

2六、線程的狀態和優先級。

2七、ThreadLocal的使用

2八、Java中的併發工具(CountDownLatch,CyclicBarrier等)

2九、進程線程在操做系統中的實現

30、雙線程經過線程同步的方式打印12121212.......

3一、java線程,場景實現,多個線程如何同時請求,返回的結果如何等待全部線程數據完成後合成一個數據

3二、服務器只提供數據接收接口,在多線程或多進程條件下,如何保證數據的有序到達?

3三、單機上一個線程池正在處理服務,若是突然斷電了怎麼辦(正在處理和阻塞隊列裏的請求怎麼處理)?

Java虛擬機面試題 (⭐⭐⭐)

一、JVM內存區域。

JVM基本構成

image

從上圖可知,JVM主要包括四個部分:

1.類加載器(ClassLoader):在JVM啓動時或者在類運行將須要的class加載到JVM中。(下圖表示了從java源文件到JVM的整個過程,可配合理解。

image

2.執行引擎:負責執行class文件中包含的字節碼指令;

3.內存區(也叫運行時數據區):是在JVM運行的時候操做所分配的內存區。運行時內存區主要能夠劃分爲5個區域,如圖:

image

方法區(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就須要爲其在內存中分配虛擬機棧和本地方法棧來記錄調用方法的內容,分配程序計數器記錄指令執行的位置,這樣的內存消耗就是建立線程的內存代價。

二、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內存模型

三、描述一下GC的原理和回收策略?

提到垃圾回收,咱們能夠先思考一下,若是咱們去作垃圾回收須要解決哪些問題?

通常說來,咱們要解決三個問題:

一、回收哪些內存?

二、何時回收?

三、如何回收?

這些問題分別對應着引用管理和回收策略等方案。

提到引用,咱們都知道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自動內存管理:給對象分配內存 以及 回收分配給對象的內存。

一、對象優先在Eden分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次MinorGC。

二、大對象直接進入老年代。如很長的字符串以及數組。很長的字符串以及數組。

三、長期存活的對象將進入老年代。當對象在新生代中經歷過必定次數(默認爲15)的Minor GC後,就會被晉升到老年代中。

四、動態對象年齡斷定。爲了更好地適應不一樣程序的內存情況,虛擬機並非永遠地要求對象年齡必須達到了MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

須要更全面的理解請點擊這裏

四、類的加載器,雙親機制,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):負責加載用戶類路徑上的指定類庫,若是應用程序中沒有實現本身的類加載器,通常就是這個類加載器去加載應用程序中的類庫。

一、原理介紹

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()方法動態加載

這麼多類加載器,那麼當類在加載的時候會使用哪一個加載器呢?

這個時候就要提到類加載器的雙親委派模型,流程圖以下所示:

image

雙親委派模型的整個工做流程很是的簡單,以下所示:

若是一個類加載器收到了加載類的請求,它不會本身立去加載類,它會先去請求父類加載器,每一個層次的類加器都是如此。層層傳遞,直到傳遞到最高層的類加載器只有當 父類加載器反饋本身沒法加載這個類,纔會有當子類加載器去加載該類。

二、爲何要使用雙親委託這種模型呢?

由於這樣能夠避免重複加載,當父親已經加載了該類的時候,就沒有必要讓子ClassLoader再加載一次。

考慮到安全因素,咱們試想一下,若是不使用這種委託模式,那咱們就能夠隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在很是大的安全隱患,而雙親委託的方式,就能夠避免這種狀況,由於String已經在啓動時就被引導類加載器(BootstrcpClassLoader)加載,因此用戶自定義的ClassLoader永遠也沒法加載一個本身寫的String,除非你改變JDK中ClassLoader搜索類的默認算法。

三、可是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去掉。

五、JVM跟Art、Dalvik對比

  

六、GC收集器簡介?以及它的內存劃分怎麼樣的?

(1)簡介:

Garbage-First(G1,垃圾優先)收集器是服務類型的收集器,目標是多處理器機器、大內存機器。它高度符合垃圾收集暫停時間的目標,同時實現高吞吐量。Oracle JDK 7 update 4 以及更新發布版徹底支持G1垃圾收集器

(2)G1的內存劃分方式:

它是將堆內存被劃分爲多個大小相等的 heap 區,每一個heap區都是邏輯上連續的一段內存(virtual memory). 其中一部分區域被當成老一代收集器相同的角色(eden, survivor, old), 但每一個角色的區域個數都不是固定的。這在內存使用上提供了更多的靈活性

七、Java的虛擬機JVM的兩個內存:棧內存和堆內存的區別是什麼?

Java把內存劃分紅兩種:一種是棧內存,一種是堆內存。二者的區別是:

1)棧內存:在函數中定義的一些基本類型的變量和對象的引用變量都在函數的棧內存中分配。 當在一段代碼塊定義一個變量時,Java就在棧中爲這個變量分配內存空間,當超過變量的做用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間能夠當即被另做他用。

2)堆內存:堆內存用來存放由new建立的對象和數組。在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。

八、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:MetaspaceSize指定

-XX:MaxPermSize

  指定方法區的最大值, 默認是物理內存的1/4, 在java8中由-XX:MaxMetaspaceSize指定元數據區的大小

-XX:NewRatio=n

  年老代與年輕代的比值,-XX:NewRatio=2, 表示年老代與年輕代的比值爲2:1

-XX:SurvivorRatio=n

  Eden區與Survivor區的大小比值,-XX:SurvivorRatio=8表示Eden區與Survivor區的大小比值是8:1:1,由於Survivor區有兩個(from, to)

九、jstack,jmap,jutil分別的意義?如何線上排查JVM的相關問題?

十、JVM方法區存儲內容 是否會動態擴展 是否會出現內存溢出 出現的緣由有哪些。

十一、如何解決同時存在的對象建立和對象回收問題?

十二、JVM中最大堆大小有沒有限制?

1三、JVM方法區存儲內容 是否會動態擴展 是否會出現內存溢出 出現的緣由有哪些。

1四、如何理解Java的虛函數表?

1五、Java運行時數據區域,致使內存溢出的緣由。

1六、對象建立、內存佈局,訪問定位等。

相關文章
相關標籤/搜索