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

前言

成爲一名優秀的Android開發,需要一份完備的知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。

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

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

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

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 的初始大小是0,然後,當add第一個元素的時候大小則變成10。並且,在後續擴容的時候會變成當前容量的1.5倍大小。

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 是二叉樹(紅黑樹的樹據結構)實現的,TreeSet中的數據是自動排好序的,不允許放入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的匿名內部類有哪些限制?

考察匿名內部類的概念和用法(初級)
  • 匿名內部類的名字:沒有人類認知意義上的名字
  • 只能繼承一個父類或實現一個接口
  • 包名.OuterClass1,表示定位的第一個匿名內部類。外部類加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、添加新方法:如果你往抽象類中添加新的方法,你可以給它提供默認的實現。因此你不需要改變你現在的代碼。如果你往接口中添加方法,那麼你必須改變實現該接口的類。

需要注意的是, 在 JDK V1.8 及之後的版本中,在 Interface 中增加了 defalut 方法,即接口默認方法。該新特性允許我們在接口中添加一個非抽象的方法實現,而這樣做的方法只需要使用關鍵字default修飾該默認實現方法即可。一個簡單的示例如下所示:

public interface Formula {
    double calculate(int a);
    default double sqrt(int a){
        return Math.sqrt(a);
    }
}
複製代碼

該特性又叫擴展方法。通過該特性,我們將能夠很方便的實現接口默認實現類。

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,不過它要保證線程安全,所以處理起來要麻煩些。

初始化槽: ensureSegment

ConcurrentHashMap 初始化的時候會初始化第一個槽 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介紹?

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

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: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)

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

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

11、如何解決同時存在的對象創建和對象回收問題?

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

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

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

15、Java運行時數據區域,導致內存溢出的原因。

16、對象創建、內存佈局,訪問定位等。

公衆號

我的公衆號 JsonChao 開通啦,如果您想第一時間獲取最新文章和最新動態,歡迎掃描關注~

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