天天看點

2020年中進階Android面試秘籍(Java篇)前言Java面試題

轉載:https://juejin.im/post/5e5c5c52f265da575f4e7558

前言

成為一名優秀的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集合的類結構圖如下所示:

2020年中進階Android面試秘籍(Java篇)前言Java面試題

List:有序、可重複;索引查詢速度快;插入、删除伴随資料移動,速度慢;

Set:無序,不可重複;

Map:鍵值對,鍵唯一,值多個;

1.List,Set都是繼承自Collection接口,Map則不是;

2.List特點:元素有放入順序,元素可重複;

Set特點:元素無放入順序,元素不可重複,重複元素會蓋掉,(注意:元素雖然無放入順序,但是元素在set中位置是由該元素的HashCode決定的,其位置其實是固定,加入Set 的Object必須定義equals()方法;

另外list支援for循環,也就是通過下标來周遊,也可以使用疊代器,但是set隻能用疊代,因為他無序,無法用下标取得想要的值)。

3.Set和List對比:

Set:檢索元素效率低下,删除和插入效率高,插入和删除不會引起元素位置改變。

List:和數組類似,List可以動态增長,查找元素效率高,插入删除元素效率低,因為會引起其他元素位置改變。

4.Map适合儲存鍵值對的資料。

5.線程安全集合類與非線程安全集合類

LinkedList、ArrayList、HashSet是非線程安全的,Vector是線程安全的;

HashMap是非線程安全的,HashTable是線程安全的;

StringBuilder是非線程安全的,StringBuffer是線程安的。

下面是這些類具體的使用介紹:

ArrayList與LinkedList的差別和适用場景

Arraylist:

優點:ArrayList是實作了基于動态數組的資料結構,因位址連續,一旦資料存儲好了,查詢操作效率會比較高(在記憶體裡是連着放的)。

缺點:因為位址連續,ArrayList要移動資料,是以插入和删除操作效率比較低。

LinkedList:

優點:LinkedList基于連結清單的資料結構,位址是任意的,其在開辟記憶體空間的時候不需要等一個連續的位址,對新增和删除操作add和remove,LinedList比較占優勢。LikedList 适用于要頭尾操作或插入指定位置的場景。

缺點:因為LinkedList要移動指針,是以查詢操作性能比較低。

适用場景分析:

當需要對資料進行對此通路的情況下選用ArrayList,當要對資料進行多次增加删除修改時采用LinkedList。

ArrayList和LinkedList怎麼動态擴容的嗎?

ArrayList:

ArrayList 初始化大小是 10 (如果你知道你的arrayList 會達到多少容量,可以在初始化的時候就指定,能節省擴容的性能開支) 擴容點規則是,新增的時候發現容量不夠用了,就去擴容 擴容大小規則是,擴容後的大小= 原始大小+原始大小/2 + 1。(例如:原始大小是 10 ,擴容後的大小就是 10 + 5+1 = 16)

LinkedList:

linkedList 是一個雙向連結清單,沒有初始化大小,也沒有擴容的機制,就是一直在前面或者後面新增就好。

ArrayList與Vector的差別和适用場景

ArrayList有三個構造方法:

public ArrayList(intinitialCapacity)// 構造一個具有指定初始容量的空清單。   
public ArrayList()// 構造一個初始容量為10的空清單。
public ArrayList(Collection<? extends E> c)// 構造一個包含指定 collection 的元素的清單  
複制代碼
           

Vector有四個構造方法:

public Vector() // 使用指定的初始容量和等于零的容量增量構造一個空向量。    
public Vector(int initialCapacity) // 構造一個空向量,使其内部資料數組的大小,其标準容量增量為零。    
public Vector(Collection<? extends E> c)// 構造一個包含指定 collection 中的元素的向量  
public Vector(int initialCapacity, int capacityIncrement)// 使用指定的初始容量和容量增量構造一個空的向量
複制代碼
           

ArrayList和Vector都是用數組實作的,主要有這麼四個差別:

1)Vector是多線程安全的,線程安全就是說多線程通路代碼,不會産生不确定的結果。而ArrayList不是,這可以從源碼中看出,Vector類中的方法很多有synchronied進行修飾,這樣就導緻了Vector在效率上無法與ArrayLst相比;

2)兩個都是采用的線性連續空間存儲元素,但是當空間充足的時候,兩個類的增加方式是不同。

3)Vector可以設定增長因子,而ArrayList不可以。

4)Vector是一種老的動态數組,是線程同步的,效率很低,一般不贊成使用。

适用場景:

1.Vector是線程同步的,是以它也是線程安全的,而ArraList是線程異步的,是不安全的。如果不考慮到線程的安全因素,一般用ArrayList效率比較高。

2.如果集合中的元素的數目大于目前集合數組的長度時,在集合中使用資料量比較大的資料,用Vector有一定的優勢。

HashSet與TreeSet的差別和适用場景

1.TreeSet 是二叉樹(紅黑樹的樹據結構)實作的,Treest中的資料是自動排好序的,不允許放入null值。

2.HashSet 是哈希表實作的,HashSet中的資料是無序的可以放入null,但隻能放入一個null,兩者中的值都不重複,就如資料庫中唯一限制。

3.HashSet要求放入的對象必須實作HashCode()方法,放的對象,是以hashcode碼作為辨別的,而具有相同内容的String對象,hashcode是一樣,是以放入的内容不能重複但是同一個類的對象可以放入不同的執行個體。

适用場景分析:

HashSet是基于Hash算法實作的,其性能通常都優于TreeSet。為快速查找而設計的Set,我們通常都應該使用HashSet,在我們需要排序的功能時,我們才使用TreeSet。

HashMap與TreeMap、HashTable的差別及适用場景

HashMap 非線程安全

HashMap:基于哈希表(散清單)實作。使用HashMap要求的鍵類明确定義了hashCode()和equals()[可以重寫hasCode()和equals()],為了優化HashMap空間的使用,您可以調優初始容量和負載因子。其中散清單的沖突處理主分兩種,一種是開放定址法,另一種是連結清單法。HashMap實作中采用的是連結清單法。

TreeMap:非線程安全基于紅黑樹實作。TreeMap沒有調優選項,因為該樹總處于平衡狀态。

适用場景分析:

HashMap和HashTable:HashMap去掉了HashTable的contain方法,但是加上了containsValue()和containsKey()方法。HashTable是同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允許空鍵值,而HashTable不允許。

HashMap:适用于Map中插入、删除和定位元素。

Treemap:适用于按自然順序或自定義順序周遊鍵(key)。 (ps:其實我們工作的過程中對集合的使用是很頻繁的,稍注意并總結積累一下,在面試的時候應該會回答的很輕松)

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

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

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

3、HashMap和HashTable的主要差別是什麼?,兩者底層實作的資料結構是什麼?

HashMap和HashTable的差別:

二者都實作了Map 接口,是将唯一的鍵映射到特定的值上,主要差別在于:

1)HashMap 沒有排序,允許一個null 鍵和多個null 值,而Hashtable 不允許;

2)HashMap 把Hashtable 的contains 方法去掉了,改成containsvalue 和containsKey, 因為contains 方法容易讓人引起誤解;

3)Hashtable 繼承自Dictionary 類,HashMap 是Java1.2 引進的Map 接口的實作;

4)Hashtable 的方法是Synchronized 的,而HashMap 不是,在多個線程通路Hashtable 時,不需要自己為它的方法實作同步,而HashMap 就必須為之提供額外的同步。Hashtable 和HashMap 采用的hash/rehash 算法大緻一樣,是以性能不會有很大的差異。

HashMap和HashTable的底層實作資料結構:

HashMap和Hashtable的底層實作都是數組 + 連結清單結構實作的(jdk8以前)

4、HashMap、ConcurrentHashMap、hash()相關原了解析?

HashMap 1.7的原理:

HashMap 底層是基于 數組 + 連結清單 組成的,不過在 jdk1.7 和 1.8 中具體實作稍有不同。

負載因子:

  • 給定的預設容量為 16,負載因子為 0.75。Map 在使用過程中不斷的往裡面存放資料,當數量達到了 16 * 0.75 = 12 就需要将目前 16 的容量進行擴容,而擴容這個過程涉及到 rehash、複制資料等操作,是以非常消耗性能。
  • 是以通常建議能提前預估 HashMap 的大小最好,盡量的減少擴容帶來的性能損耗。

其實真正存放資料的是 Entry<K,V>[] table,Entry 是 HashMap 中的一個靜态内部類,它有key、value、next、hash(key的hashcode)成員變量。

put 方法:

  • 判斷目前數組是否需要初始化。
  • 如果 key 為空,則 put 一個空值進去。
  • 根據 key 計算出 hashcode。
  • 根據計算出的 hashcode 定位出所在桶。
  • 如果桶是一個連結清單則需要周遊判斷裡面的 hashcode、key 是否和傳入 key 相等,如果相等則進行覆寫,并傳回原來的值。
  • 如果桶是空的,說明目前位置沒有資料存入,新增一個 Entry 對象寫入目前位置。(當調用 addEntry 寫入 Entry 時需要判斷是否需要擴容。如果需要就進行兩倍擴充,并将目前的 key 重新 hash 并定位。而在 createEntry 中會将目前位置的桶傳入到建立的桶中,如果目前桶有值就會在位置形成連結清單。)

get 方法:

  • 首先也是根據 key 計算出 hashcode,然後定位到具體的桶中。
  • 判斷該位置是否為連結清單。
  • 不是連結清單就根據 key、key 的 hashcode 是否相等來傳回值。
  • 為連結清單則需要周遊直到 key 及 hashcode 相等時候就傳回值。
  • 啥都沒取到就直接傳回 null 。

HashMap 1.8的原理:

當 Hash 沖突嚴重時,在桶上形成的連結清單會變的越來越長,這樣在查詢時的效率就會越來越低;時間複雜度為 O(N),是以 1.8 中重點優化了這個查詢效率。

TREEIFY_THRESHOLD 用于判斷是否需要将連結清單轉換為紅黑樹的門檻值。

HashEntry 修改為 Node。

put 方法:

  • 判斷目前桶是否為空,空的就需要初始化(在resize方法 中會判斷是否進行初始化)。
  • 根據目前 key 的 hashcode 定位到具體的桶中并判斷是否為空,為空表明沒有 Hash 沖突就直接在目前位置建立一個新桶即可。
  • 如果目前桶有值( Hash 沖突),那麼就要比較目前桶中的 key、key 的 hashcode 與寫入的 key 是否相等,相等就指派給 e,在第 8 步的時候會統一進行指派及傳回。
  • 如果目前桶為紅黑樹,那就要按照紅黑樹的方式寫入資料。
  • 如果是個連結清單,就需要将目前的 key、value 封裝成一個新節點寫入到目前桶的後面(形成連結清單)。
  • 接着判斷目前連結清單的大小是否大于預設的門檻值,大于時就要轉換為紅黑樹。
  • 如果在周遊過程中找到 key 相同時直接退出周遊。
  • 如果 e != null 就相當于存在相同的 key,那就需要将值覆寫。
  • 最後判斷是否需要進行擴容。

get 方法:

  • 首先将 key hash 之後取得所定位的桶。
  • 如果桶為空則直接傳回 null 。
  • 否則判斷桶的第一個位置(有可能是連結清單、紅黑樹)的 key 是否為查詢的 key,是就直接傳回 value。
  • 如果第一個不比對,則判斷它的下一個是紅黑樹還是連結清單。
  • 紅黑樹就按照樹的查找方式傳回值。
  • 不然就按照連結清單的方式周遊比對傳回值。

修改為紅黑樹之後查詢效率直接提高到了 O(logn)。但是 HashMap 原有的問題也都存在,比如在并發場景下使用時容易出現死循環:

  • 在 HashMap 擴容的時候會調用 resize() 方法,就是這裡的并發操作容易在一個桶上形成環形連結清單;這樣當擷取一個不存在的 key 時,計算出的 index 正好是環形連結清單的下标就會出現死循環:在 1.7 中 hash 沖突采用的頭插法形成的連結清單,在并發條件下會形成循環連結清單,一旦有查詢落到了這個連結清單上,當擷取不到值時就會死循環。

ConcurrentHashMap 1.7原理:

ConcurrentHashMap 采用了分段鎖技術,其中 Segment 繼承于 ReentrantLock。不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同步處理,理論上 ConcurrentHashMap 支援 CurrencyLevel (Segment 數組數量)的線程并發。每當一個線程占用鎖通路一個 Segment 時,不會影響到其他的 Segment。

put 方法:

首先是通過 key 定位到 Segment,之後在對應的 Segment 中進行具體的 put。

  • 雖然 HashEntry 中的 value 是用 volatile 關鍵詞修飾的,但是并不能保證并發的原子性,是以 put 操作時仍然需要加鎖處理。
  • 首先第一步的時候會嘗試擷取鎖,如果擷取失敗肯定就有其他線程存在競争,則利用 scanAndLockForPut() 自旋擷取鎖:

    嘗試自旋擷取鎖。 如果重試的次數達到了 MAX_SCAN_RETRIES 則改為阻塞鎖擷取,保證能擷取成功。

  • 将目前 Segment 中的 table 通過 key 的 hashcode 定位到 HashEntry。
  • 周遊該 HashEntry,如果不為空則判斷傳入的 key 和目前周遊的 key 是否相等,相等則覆寫舊的 value。
  • 為空則需要建立一個 HashEntry 并加入到 Segment 中,同時會先判斷是否需要擴容。
  • 最後會使用unlock()解除目前 Segment 的鎖。

get 方法:

  • 隻需要将 Key 通過 Hash 之後定位到具體的 Segment ,再通過一次 Hash 定位到具體的元素上。
  • 由于 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了記憶體可見性,是以每次擷取時都是最新值。
  • ConcurrentHashMap 的 get 方法是非常高效的,因為整個過程都不需要加鎖。

ConcurrentHashMap 1.8原理:

1.7 已經解決了并發問題,并且能支援 N 個 Segment 這麼多次數的并發,但依然存在 HashMap 在 1.7 版本中的問題:那就是查詢周遊連結清單效率太低。和 1.8 HashMap 結構類似:其中抛棄了原有的 Segment 分段鎖,而采用了 CAS + synchronized 來保證并發安全性。

CAS:

如果obj内的value和expect相等,就證明沒有其他線程改變過這個變量,那麼就更新它為update,如果這一步的CAS沒有成功,那就采用自旋的方式繼續進行CAS操作。

問題:

  • 目前在JDK的atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查目前引用是否等于預期引用,并且目前标志是否等于預期标志,如果全部相等,則以原子方式将該引用和該标志的值設定為給定的更新值。
  • 如果CAS不成功,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執行開銷。

put 方法:

  • 根據 key 計算出 hashcode 。
  • 判斷是否需要進行初始化。
  • 如果目前 key 定位出的 Node為空表示目前位置可以寫入資料,利用 CAS 嘗試寫入,失敗則自旋保證成功。
  • 如果目前位置的 hashcode == MOVED == -1,則需要進行擴容。
  • 如果都不滿足,則利用 synchronized 鎖寫入資料。
  • 最後,如果數量大于 TREEIFY_THRESHOLD 則要轉換為紅黑樹。

get 方法:

  • 根據計算出來的 hashcode 尋址,如果就在桶上那麼直接傳回值。
  • 如果是紅黑樹那就按照樹的方式擷取值。
  • 就不滿足那就按照連結清單的方式周遊擷取值。

1.8 在 1.7 的資料結構上做了大的改動,采用紅黑樹之後可以保證查詢效率(O(logn)),甚至取消了 ReentrantLock 改為了 synchronized,這樣可以看出在新版的 JDK 中對 synchronized 優化是很到位的。

HashMap、ConcurrentHashMap 1.7/1.8實作原理

hash()算法全解析

HashMap何時擴容:

當向容器添加元素的時候,會判斷目前容器的元素個數,如果大于等于門檻值---即大于目前數組的長度乘以加載因子的值的時候,就要自動擴容。

擴容的算法是什麼:

擴容(resize)就是重新計算容量,向HashMap對象裡不停的添加元素,而HashMap對象内部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。當然Java裡的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組。

Hashmap如何解決散列碰撞(必問)?

Java中HashMap是利用“拉鍊法”處理HashCode的碰撞問題。在調用HashMap的put方法或get方法時,都會首先調用hashcode方法,去查找相關的key,當有沖突時,再調用equals方法。hashMap基于hasing原理,我們通過put和get方法存取對象。當我們将鍵值對傳遞給put方法時,他調用鍵對象的hashCode()方法來計算hashCode,然後找到bucket(哈希桶)位置來存儲對象。當擷取對象時,通過鍵對象的equals()方法找到正确的鍵值對,然後傳回值對象。HashMap使用連結清單來解決碰撞問題,當碰撞發生了,對象将會存儲在連結清單的下一個節點中。hashMap在每個連結清單節點存儲鍵值對對象。當兩個不同的鍵卻有相同的hashCode時,他們會存儲在同一個bucket位置的連結清單中。鍵對象的equals()來找到鍵值對。

Hashmap底層為什麼是線程不安全的?

  • 并發場景下使用時容易出現死循環,在 HashMap 擴容的時候會調用 resize() 方法,就是這裡的并發操作容易在一個桶上形成環形連結清單;這樣當擷取一個不存在的 key 時,計算出的 index 正好是環形連結清單的下标就會出現死循環;
  • 在 1.7 中 hash 沖突采用的頭插法形成的連結清單,在并發條件下會形成循環連結清單,一旦有查詢落到了這個連結清單上,當擷取不到值時就會死循環。

5、ArrayMap跟SparseArray在HashMap上面的改進?

HashMap要存儲完這些資料将要不斷的擴容,而且在此過程中也需要不斷的做hash運算,這将對我們的記憶體空間造成很大消耗和浪費。

SparseArray:

SparseArray比HashMap更省記憶體,在某些條件下性能更好,主要是因為它避免了對key的自動裝箱(int轉為Integer類型),它内部則是通過兩個數組來進行資料存儲的,一個存儲key,另外一個存儲value,為了優化性能,它内部對資料還采取了壓縮的方式來表示稀疏數組的資料,進而節約記憶體空間,我們從源碼中可以看到key和value分别是用數組表示:

private int[] mKeys;
private Object[] mValues;
複制代碼
           

同時,SparseArray在存儲和讀取資料時候,使用的是二分查找法。也就是在put添加資料的時候,會使用二分查找法和之前的key比較目前我們添加的元素的key的大小,然後按照從小到大的順序排列好,是以,SparseArray存儲的元素都是按元素的key值從小到大排列好的。 而在擷取資料的時候,也是使用二分查找法判斷元素的位置,是以,在擷取資料的時候非常快,比HashMap快的多。

ArrayMap:

ArrayMap利用兩個數組,mHashes用來儲存每一個key的hash值,mArrray大小為mHashes的2倍,依次儲存key和value。

mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
複制代碼
           

當插入時,根據key的hashcode()方法得到hash值,計算出在mArrays的index位置,然後利用二分查找找到對應的位置進行插入,當出現哈希沖突時,會在index的相鄰位置插入。

假設資料量都在千級以内的情況下:

1、如果key的類型已經确定為int類型,那麼使用SparseArray,因為它避免了自動裝箱的過程,如果key為long類型,它還提供了一個LongSparseArray來確定key為long類型時的使用

2、如果key類型為其它的類型,則使用ArrayMap。

三、反射 (⭐⭐⭐)

1、說說你對Java反射的了解?

答:Java 中的反射首先是能夠擷取到Java中要反射類的位元組碼, 擷取位元組碼有三種方法:

1.Class.forName(className)

2.類名.class

3.this.getClass()。

然後将位元組碼中的方法,變量,構造函數等映射成相應的Method、Filed、Constructor等類,這些類提供了豐富的方法可以被我們所使用。

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

Java基礎之—反射(非常重要)

四、泛型 (⭐⭐)

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

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

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

泛型的好處是在編譯的時候檢查類型安全,并且所有的轉換都是自動和隐式的,提高代碼的重用率。

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

2、同一種泛型可以對應多個版本(因為參數類型是不确的),不同版本的泛型類執行個體是不相容的。

3、泛型的類型參數可以有多個。

4、泛型的參數類型可以使用extends語句,例如。習慣上稱為“有界類型”。

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

泛型擦除以及相關的概念

泛型資訊隻存在代碼編譯階段,在進入JVM之前,與泛型關的資訊都會被擦除掉。

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

Java中的泛型基本上都是在編譯器這個層次來實作的。生成的Java位元組碼中是不包含泛型中的類型資訊的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候擦除掉。這個過程就稱為類型擦除。

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

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

2、自動類型轉換

3、類型擦除與多态的沖突和解決方法

4、泛型類型變量不能是基本資料類型

5、運作時類型查詢

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

7、數組(這個不屬于類型擦除引起的問題)

9、類型擦除後的沖突

10、泛型在靜态方法和靜态類中的問題

五、注解 (⭐⭐)

1、說說你對Java注解的了解?

注解相當于一種标記,在程式中加了注解就等于為程式打上了某種标記。程式可以利用ava的反射機制來了解你的類及各種元素上有無何種标記,針對不同的标記,就去做相應的事件。标記可以加在包,類,字段,方法,方法的參數以及局部變量上。

六、其它 (⭐⭐)

1、Java的char是兩個位元組,是怎麼存Utf-8的字元的?

是否熟悉Java char和字元串(初級)

  • char是2個位元組,utf-8是1~3個位元組。
  • 字元集(字元集不是編碼):ASCII碼與Unicode碼。
  • 字元 -> 0xd83dde00(碼點)。

是否了解字元的映射和存儲細節(中級)

人類認知:字元 => 字元集:0x4e2d(char) => 計算機存儲(byte):01001110:4e、00101101:2d

編碼:UTF-16

“中”.getBytes("utf-6"); -> fe ff 4e 2d:4個位元組,其中前面的fe ff隻是位元組序标志。

是否能觸類旁通,橫向對比其他語言(進階)

Python2的字元串:

  • byteString = "中"
  • unicodeString = u"中"

令人迷惑的字元串長度

emoij = u"表情"
print(len(emoji)
複制代碼
           

Java與python 3.2及以下:2位元組 python >= 3.3:1位元組

注意:Java 9對latin字元的存儲空間做了優化,但字元串長度還是!= 字元數。

總結

  • Java char不存UTF-8的位元組,而是UTF-16。
  • Unicode通用字元集占兩個位元組,例如“中”。
  • Unicode擴充字元集需要用一對char來表示,例如“表情”。
  • Unicode是字元集,不是編碼,作用類似于ASCII碼。
  • Java String的length不是字元數。

2、Java String可以有多長?

是否對字元串編解碼有深入了解(中級)

配置設定到棧:

String longString = "aaa...aaa";
複制代碼
           

配置設定到堆:

byte[] bytes = loadFromFile(new File("superLongText.txt");
String superLongString = new String(bytes);
複制代碼
           

是否對字元串在記憶體當中的存儲形式有深入了解(進階)

是否對Java虛拟機位元組碼有足夠的了解(進階)

源檔案:*.java

String longString = "aaa...aaa";
位元組數 <= 65535
複制代碼
           

位元組碼:*.class

CONSTANT_Utf8_info { 
    u1 tag; 
    u2 length;
    (0~65535) u1 bytes[length]; 
    最多65535個位元組 
}
複制代碼
           

javac的編譯器有問題,< 65535應該改為< = 65535。

Java String 棧配置設定

  • 受位元組碼限制,字元串最終的MUTF-8位元組數不超過65535。
  • Latin字元,受Javac代碼限制,最多65534個。
  • 非Latin字元最終對應位元組個數差異較大,最多位元組個數是65535。
  • 如果運作時方法區設定較小,也會受到方法區大小的限制。

是否對java虛拟機指令有一定的認識(進階)

new String(bytes)内部是采用了一個字元數組,其對應的虛拟機指令是newarray [int] ,數組理論最大個數為Integer.MAX_VALUE,有些虛拟機需要一些頭部資訊,是以MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8。

Java String 堆配置設定

  • 受虛拟機指令限制,字元數理論上限為Integer.MAX_VALUE。
  • 受虛拟機實作限制,實際上限可能會小于Integer.MAX_VALUE。
  • 如果堆記憶體較小,也會受到堆記憶體的限制。

總結

Java String字面量形式

  • 位元組碼中CONSTANT_Utf8_info的限制
  • Javac源碼邏輯的限制
  • 方法區大小的限制

Java String運作時建立在堆上的形式

  • Java虛拟機指令newarray的限制
  • Java虛拟機堆記憶體大小的限制

3、Java的匿名内部類有哪些限制?

考察匿名内部類的概念和用法(初級)

  • 匿名内部類的名字:沒有人類認知意義上的名字
  • 隻能繼承一個父類或實作一個接口
  • 包名.OuterClass
    2020年中進階Android面試秘籍(Java篇)前言Java面試題
    N,N是匿名内部類的順序。

考察語言規範以及語言的橫向對比等(中級)

匿名内部類的繼承結構:Java中的匿名内部類不可以繼承,隻有内部類才可以有實作繼承、實作接口的特性。而Kotlin是的匿名内部類是支援繼承的,如

val runnableFoo = object: Foo(),Runnable { 
        override fun run() { 
        
        } 
}
複制代碼
           

作為考察記憶體洩漏的切入點(進階)

匿名内部類的構造方法(深入源碼位元組碼探索語言本質的能力):

  • 匿名内部類會預設持有外部類的引用,可能會導緻記憶體洩漏。
  • 由編譯器生成的。

其參數清單包括

  • 外部對象(定義在非靜态域内)
  • 父類的外部對象(父類非靜态)
  • 父類的構造方法參數(父類有構造方法且參數清單不為空)
  • 外部捕獲的變量(方法體内有引用外部final變量)

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

如果CallBack是一個interface,不是抽象類,則可以轉換為Lambda表達式。

CallBack callBack = () -> { 
        ... 
};
複制代碼
           

總結

  • 沒有人類認知意義上的名字。
  • 隻能繼承一個父類或實作一個接口。
  • 父類是非靜态的類型,則需父類外部執行個體來初始化。
  • 如果定義在非靜态作用域内,會引用外部類執行個體。
  • 隻能捕獲外部作用域内的final變量。
  • 建立時隻有單一方法的接口可以用Lambda轉換。

技巧點撥

關注語言版本的變化:

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

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

異常整體分類:

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

Error是程式無法處理的錯誤,比如OutOfMemoryError、StackOverflowError。這些異常發生時, Java虛拟機(JVM)一般會選擇線程終止。

Exception是程式本身可以處理的異常,這種異常分兩大類運作時異常和非運作時異常,程式中應當盡可能去處理這些異常。

運作時異常都是RuntimeException類及其子類異常,如NullPointerException、IndexOutOfBoundsException等, 這些異常是不檢查異常,程式中可以選擇捕獲處理,也可以不處理。這些異常一般是由程式邏輯錯誤引起的, 程式應該從邏輯角度盡可能避免這類異常的發生。

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

1、盡量不要捕獲類似 Exception 這樣的通用異常,而是應該捕獲特定異常。

2、不要生吞異常。

NoClassDefFoundError 和 ClassNotFoundException 有什麼差別?

ClassNotFoundException的産生原因主要是: Java支援使用反射方式在運作時動态加載類,例如使用Class.forName方法來動态地加載類時,可以将類名作為參數傳遞給上述方法進而将指定類加載到JVM記憶體中,如果這個類在類路徑中沒有被找到,那麼此時就會在運作時抛出ClassNotFoundException異常。 解決該問題需要確定所需的類連同它依賴的包存在于類路徑中,常見問題在于類名書寫錯誤。 另外還有一個導緻ClassNotFoundException的原因就是:當一個類已經某個類加載器加載到記憶體中了,此時另一個類加載器又嘗試着動态地從同一個包中加載這個類。通過控制動态類加載過程,可以避免上述情況發生。

NoClassDefFoundError産生的原因在于: 如果JVM或者ClassLoader執行個體嘗試加載(可以通過正常的方法調用,也可能是使用new來建立新的對象)類的時候卻找不到類的定義。要查找的類在編譯的時候是存在的,運作的時候卻找不到了。這個時候就會導緻NoClassDefFoundError. 造成該問題的原因可能是打包過程漏掉了部分類,或者jar包出現損壞或者篡改。解決這個問題的辦法是查找那些在開發期間存在于類路徑下但在運作期間卻不在類路徑下的類。

5、String 為什麼要設計成不可變的?

String是不可變的(修改String時,不會在原有的記憶體位址修改,而是重新指向一個新對象),String用final修飾,不可繼承,String本質上是個final的char[]數組,是以char[]數組的記憶體位址不會被修改,而且String 也沒有對外暴露修改char[]數組的方法。不可變性可以保證線程安全以及字元串串常量池的實作。

6、Java裡的幂等性了解嗎?

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

幂等性最為常見的應用就是電商的客戶付款,試想一下如果你在付款的時候因為網絡等各種問題失敗了,然後去重複的付了一次,是一種多麼糟糕的體驗。幂等性就是為了解決這樣的問題。

實作幂等性可以使用Token機制。

核心思想是為每一次操作生成一個唯一性的憑證,也就是token。一個token在操作的每一個階段隻有一次執行權,一旦執行成功則儲存執行結果。對重複的請求,傳回同一個結果。

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

7、為什麼Java裡的匿名内部類隻能通路final修飾的外部變量?

匿名内部類用法:

public class TryUsingAnonymousClass {
    public void useMyInterface() {
        final Integer number = 123;
        System.out.println(number);

        MyInterface myInterface = new MyInterface() {
            @Override
            public void doSomething() {
                System.out.println(number);
            }
        };
        myInterface.doSomething();

        System.out.println(number);
    }
}
複制代碼
           

編譯後的結果

class TryUsingAnonymousClass$1
        implements MyInterface {
    private final TryUsingAnonymousClass this$0;
    private final Integer paramInteger;

    TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer paramInteger) {
        this.this$0 = this$0;
        this.paramInteger = paramInteger;
    }

    public void doSomething() {
        System.out.println(this.paramInteger);
    }
}
複制代碼
           

因為匿名内部類最終會編譯成一個單獨的類,而被該類使用的變量會以構造函數參數的形式傳遞給該類,例如:Integer paramInteger,如果變量不定義成final的,paramInteger在匿名内部類被可以被修改,進而造成和外部的paramInteger不一緻的問題,為了避免這種不一緻的情況,因次Java規定匿名内部類隻能通路final修飾的外部變量。

8、講一下Java的編碼方式?

為什麼需要編碼

計算機存儲資訊的最小單元是一個位元組即8bit,是以能示的範圍是0~255,這個範圍無法儲存所有的字元,是以要一個新的資料結構char來表示這些字元,從char到byte需要編碼。

常見的編碼方式有以下幾種:

ASCII:總共有 128 個,用一個位元組的低 7 位表示,031 是控制字元如換行回車删除等;32126 是列印字元,可以通過鍵盤輸入并且能夠顯示出來。

GBK:碼範圍是 8140~FEFE(去掉 XX7F)總共有 23940 個碼位,它能表示 21003 個漢字,它的編碼是和 GB2312 相容的,也就是說用 GB2312 編碼的漢字可以用 GBK 來解碼,并且不會有亂碼。

UTF-16:UTF-16 具體定義了 Unicode 字元在計算機中存取方法。UTF-16 用兩個位元組來表示 Unicode 轉化格式,這個是定長的表示方法,不論什麼字元都可以用兩個位元組表示,兩個位元組是 16 個 bit,是以叫 UTF-16。UTF-16 表示字元非常友善,每兩個位元組表示一個字元,這個在字元串操作時就大大簡化了操作,這也是 Java 以 UTF-16 作為記憶體的字元存儲格式的一個很重要的原因。

UTF-8:統一采用兩個位元組表示一個字元,雖然在表示上非常簡單友善,但是也有其缺點,有很大一部分字元用一個位元組就可以表示的現在要兩個位元組表示,存儲空間放大了一倍,在現在的網絡帶寬還非常有限的今天,這樣會增大網絡傳輸的流量,而且也沒必要。而 UTF-8 采用了一種變長技術,每個編碼區域有不同的字碼長度。不同類型的字元可以是由 1~6 個位元組組成。

Java中需要編碼的地方一般都在字元到位元組的轉換上,這個一般包括磁盤IO和網絡IO。

Reader 類是 Java 的 I/O 中讀字元的父類,而InputStream 類是讀位元組的父類,InputStreamReader類就是關聯位元組到字元的橋梁,它負責在 I/O 過程中處理讀取位元組到字元的轉換,而具體位元組到字元解碼實作由 StreamDecoder 去實作,在 StreamDecoder 解碼過程中必須由使用者指定 Charset 編碼格式。

9、String,StringBuffer,StringBuilder有哪些不同?

三者在執行速度方面的比較:StringBuilder >  StringBuffer  >  String

String每次變化一個值就會開辟一個新的記憶體空間

StringBuilder:線程非安全的

StringBuffer:線程安全的

對于三者使用的總結:

1.如果要操作少量的資料用 String。

2.單線程操作字元串緩沖區下操作大量資料用 StringBuilder。

3.多線程操作字元串緩沖區下操作大量資料用 StringBuffer。

String 是 Java 語言非常基礎和重要的類,提供了構造和管理字元串的各種基本邏輯。它是典型的 Immutable 類,被聲明成為 final class,所有屬性也都是 final 的。也由于它的不可變性,類似拼接、裁剪字元串等動作,都會産生新的 String 對象。由于字元串操作的普遍性,是以相關操作的效率往往對應用性能有明顯影響。

StringBuffer 是為解決上面提到拼接産生太多中間對象的問題而提供的一個類,我們可以用 append 或者 add 方法,把字元串添加到已有序列的末尾或者指定位置。StringBuffer 本質是一個線程安全的可修改字元序列,它保證了線程安全,也随之帶來了額外的性能開銷,是以除非有線程安全的需要,不然還是推薦使用它的後繼者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質差別,但是它去掉了線程安全的部分,有效減小了開銷,是絕大部分情況下進行字元串拼接的首選。

10、什麼是内部類?内部類的作用。

内部類可以有多個執行個體,每個執行個體都有自己的狀态資訊,并且與其他外圍對象的資訊互相獨立。

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

建立内部類對象并不依賴于外圍類對象的建立。

内部類并沒有令人迷惑的“is-a”關系,他就是一個獨立的實體。

内部類提供了更好的封裝,除了該外圍類,其他類都不能通路。。

11、抽象類和接口差別?

共同點

  • 是上層的抽象層。
  • 都不能被執行個體化。
  • 都能包含抽象的方法,這些抽象的方法用于描述類具備的功能,但是不提供具體的實作。

差別:

  • 1、在抽象類中可以寫非抽象的方法,進而避免在子類中重複書寫他們,這樣可以提高代碼的複用性,這是抽象類的優勢,接口中隻能有抽象的方法。
  • 2、多繼承:一個類隻能繼承一個直接父類,這個父類可以是具體的類也可是抽象類,但是一個類可以實作多個接口。
  • 3、抽象類可以有預設的方法實作,接口根本不存在方法的實作。
  • 4、子類使用extends關鍵字來繼承抽象類。如果子類不是抽象類的話,它需要提供抽象類中所有聲明方法的實作。子類使用關鍵字implements來實作接口。它需要提供接口中所有聲明方法的實作。
  • 5、構造器:抽象類可以有構造器,接口不能有構造器。
  • 6、和普通Java類的差別:除了你不能執行個體化抽象類之外,抽象類和普通Java類沒有任何差別,接口是完全不同的類型。
  • 7、通路修飾符:抽象方法可以有public、protected和default修飾符,接口方法預設修飾符是public。你不可以使用其它修飾符。
  • 8、main方法:抽象方法可以有main方法并且我們可以運作它接口沒有main方法,是以我們不能運作它。
  • 9、速度:抽象類比接口速度要快,接口是稍微有點慢的,因為它需要時間去尋找在類中實作的方法。
  • 10、添加新方法:如果你往抽象類中添加新的方法,你可以給它提供預設的實作。是以你不需要改變你現在的代碼。如果你往接口中添加方法,那麼你必須改變實作該接口的類。

12、接口的意義?

規範、擴充、回調。

13、父類的靜态方法能否被子類重寫?

不能。子類繼承父類後,用相同的靜态方法和非靜态方法,這時非靜态方法覆寫父類中的方法(即方法重寫),父類的該靜态方法被隐藏(如果對象是父類則調用該隐藏的方法),另外子類可繼承父類的靜态與非靜态方法,至于方法重載我覺得它其中一要素就是在同一類中,不能說父類中的什麼方法與子類裡的什麼方法是方法重載的展現。

14、抽象類的意義?

為其子類提供一個公共的類型,封裝子類中的重複内容,定義抽象方法,子類雖然有不同的實作 但是定義是一緻的。

15、靜态内部類、非靜态内部類的了解?

靜态内部類:隻是為了降低包的深度,友善類的使用,靜态内部類适用于包含在類當中,但又不依賴與外在的類,不用使用外在類的非靜态屬性和方法,隻是為了友善管理類結構而定義。在建立靜态内部類的時候,不需要外部類對象的引用。

非靜态内部類:持有外部類的引用,可以自由使用外部類的所有變量和方法。

16、為什麼複寫equals方法的同時需要複寫hashcode方法,前者相同後者是否相同,反過來呢?為什麼?

要考慮到類似HashMap、HashTable、HashSet的這種散列的資料類型的運用,當我們重寫equals時,是為了用自身的方式去判斷兩個自定義對象是否相等,然而如果此時剛好需要我們用自定義的對象去充當hashmap的鍵值使用時,就會出現我們認為的同一對象,卻因為hash值不同而導緻hashmap中存了兩個對象,進而才需要進行hashcode方法的覆寫。

17、equals 和 hashcode 的關系?

hashcode和equals的約定關系如下:

  • 1、如果兩個對象相等,那麼他們一定有相同的哈希值(hashcode)。
  • 2、如果兩個對象的哈希值相等,那麼這兩個對象有可能相等也有可能不相等。(需要再通過equals來判斷)

18、java為什麼跨平台?

因為Java程式編譯之後的代碼不是能被硬體系統直接運作的代碼,而是一種“中間碼”——位元組碼。然後不同的硬體平台上安裝有不同的Java虛拟機(JVM),由JVM來把位元組碼再“翻譯”成所對應的硬體平台能夠執行的代碼。是以對于Java程式設計者來說,不需要考慮硬體平台是什麼。是以Java可以跨平台。

19、浮點數的精準計算

BigDecimal類進行商業計算,Float和Double隻能用來做科學計算或者是工程計算。

20、final,finally,finalize的差別?

final 可以用來修飾類、方法、變量,分别有不同的意義,final 修飾的 class 代表不可以繼承擴充,final 的變量是不可以修改的,而 final 的方法也是不可以重寫的(override)。

finally 則是 Java 保證重點代碼一定要被執行的一種機制。我們可以使用 try-finally 或者 try-catch-finally 來進行類似關閉 JDBC 連接配接、保證 unlock 鎖等動作。

finalize 是基礎類 java.lang.Object 的一個方法,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。finalize 機制現在已經不推薦使用,并且在 JDK 9 開始被标記為 deprecated。Java 平台目前在逐漸使用 java.lang.ref.Cleaner 來替換掉原有的 finalize 實作。Cleaner 的實作利用了幻象引用(PhantomReference),這是一種常見的所謂 post-mortem 清理機制。利用幻象引用和引用隊列,我們可以保證對象被徹底銷毀前做一些類似資源回收的工作,比如關閉檔案描述符(作業系統有限的資源),它比 finalize 更加輕量、更加可靠。

21、靜态内部類的設計意圖

靜态内部類與非靜态内部類之間存在一個最大的差別:非靜态内部類在編譯完成之後會隐含地儲存着一個引用,該引用是指向建立它的外圍内,但是靜态内部類卻沒有。

沒有這個引用就意味着:

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

22、Java中對象的生命周期

在Java中,對象的生命周期包括以下幾個階段:

1.建立階段(Created)

JVM 加載類的class檔案 此時所有的static變量和static代碼塊将被執行 加載完成後,對局部變量進行指派(先父後子的順序) 再執行new方法 調用構造函數 一旦對象被建立,并被分派給某些變量指派,這個對象的狀态就切換到了應用階段。

2.應用階段(In Use)

對象至少被一個強引用持有着。

3.不可見階段(Invisible)

當一個對象處于不可見階段時,說明程式本身不再持有該對象的任何強引用,雖然該這些引用仍然是存在着的。 簡單說就是程式的執行已經超出了該對象的作用域了。

4.不可達階段(Unreachable)

對象處于不可達階段是指該對象不再被任何強引用所持有。 與“不可見階段”相比,“不可見階段”是指程式不再持有該對象的任何強引用,這種情況下,該對象仍可能被JVM等系統下的某些已裝載的靜态變量或線程或JNI等強引用持有着,這些特殊的強引用被稱為”GC root”。存在着這些GC root會導緻對象的記憶體洩露情況,無法被回收。

5.收集階段(Collected)

當垃圾回收器發現該對象已經處于“不可達階段”并且垃圾回收器已經對該對象的記憶體空間重新配置設定做好準備時,則對象進入了“收集階段”。如果該對象已經重寫了finalize()方法,則會去執行該方法的終端操作。

6.終結階段(Finalized)

當對象執行完finalize()方法後仍然處于不可達狀态時,則該對象進入終結階段。在該階段是等待垃圾回收器對該對象空間進行回收。

7.對象空間重配置設定階段(De-allocated)

垃圾回收器對該對象的所占用的記憶體空間進行回收或者再配置設定了,則該對象徹底消失了,稱之為“對象空間重新配置設定階段。

23、靜态屬性和靜态方法是否可以被繼承?是否可以被重寫?以及原因?

結論:java中靜态屬性和靜态方法可以被繼承,但是不可以被重寫而是被隐藏。

原因:

1). 靜态方法和屬性是屬于類的,調用的時候直接通過類名.方法名完成,不需要繼承機制即可以調用。如果子類裡面定義了靜态方法和屬性,那麼這時候父類的靜态方法或屬性稱之為"隐藏"。如果你想要調用父類的靜态方法和屬性,直接通過父類名.方法或變量名完成,至于是否繼承一說,子類是有繼承靜态方法和屬性,但是跟執行個體方法和屬性不太一樣,存在"隐藏"的這種情況。

2). 多态之是以能夠實作依賴于繼承、接口和重寫、重載(繼承和重寫最為關鍵)。有了繼承和重寫就可以實作父類的引用指向不同子類的對象。重寫的功能是:"重寫"後子類的優先級要高于父類的優先級,但是“隐藏”是沒有這個優先級之分的。

3). 靜态屬性、靜态方法和非靜态的屬性都可以被繼承和隐藏而不能被重寫,是以不能實作多态,不能實作父類的引用可以指向不同子類的對象。非靜态方法可以被繼承和重寫,是以可以實作多态。

24、object類的equal 和hashcode 方法重寫,為什麼?

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

1、在java應用程式執行期間,如果在equals方法比較中所用的資訊沒有被修改,那麼在同一個對象上多次調用hashCode方法時必須一緻地傳回相同的整數。如果多次執行同一個應用時,不要求該整數必須相同。

2、如果兩個對象通過調用equals方法是相等的,那麼這兩個對象調用hashCode方法必須傳回相同的整數。

3、如果兩個對象通過調用equals方法是不相等的,不要求這兩個對象調用hashCode方法必須傳回不同的整數。但是程式員應該意識到對不同的對象産生不同的hash值可以提供哈希表的性能。

25、java中==和equals和hashCode的差別?

預設情況下也就是從超類Object繼承而來的equals方法與‘==’是完全等價的,比較的都是對象的記憶體位址,但我們可以重寫equals方法,使其按照我們的需求的方式進行比較,如String類重寫了equals方法,使其比較的是字元的序列,而不再是記憶體位址。在java的集合中,判斷兩個對象是否相等的規則是:

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

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

  • 強引用(FinalReference):在記憶體不足時不會被回收。平常用的最多的對象,如新建立的對象。
  • 軟引用(SoftReference):在記憶體不足時會被回收。用于實作記憶體敏感的高速緩存。
  • 弱引用(WeakReferenc):隻要GC回收器發現了它,就會将之回收。用于Map資料結構中,引用占用記憶體空間較大的對象。
  • 虛引用(PhantomReference):在回收之前,會被放入ReferenceQueue,JVM不會自動将該referent字段值設定成null。其它引用被JVM回收之後才會被放入ReferenceQueue中。用于實作一個對象被回收之前做一些清理工作。

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

1).因為new用到了Person.class,是以會先找到Person.class檔案,并加載到記憶體中;

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

3).在堆記憶體中開辟空間配置設定記憶體位址;

4).在堆記憶體中建立對象的特有屬性,并進行預設初始化;

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

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

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

8).将記憶體位址付給棧記憶體中的p變量。

28、JAVA常量池

Interger中的128(-128~127)

a.當數值範圍為-128~127時:如果兩個new出來的Integer對象,即使值相同,通過“==”比較結果為false,但兩個對直接指派,則通過“==”比較結果為“true,這一點與String非常相似。

b.當數值不在-128~127時,無論通過哪種方式,即使兩對象的值相等,通過“==”比較,其結果為false;

c.當一個Integer對象直接與一個int基本資料類型通過“==”比較,其結果與第一點相同;

d.Integer對象的hash值為數值本身;

為什麼是-128-127?

在Integer類中有一個靜态内部類IntegerCache,在IntegrCache類中有一個Integer數組,用以緩存目前數值範圍為-128~127時的Integer對象。

29、在重寫equals方法時,需要遵循哪些約定,具體介紹一下?

重寫equals方法時需要遵循通用約定:自反性、對稱性、傳遞性、一緻性、非空性

1)自反性

對于任何非null的引用值x,x.equals(x)必須傳回true。---這一點基本上不會有啥問題

2)對稱性

對于任何非null的引用值x和y,當且僅當x.equals(y)為true時,y.equals(x)也為true。

3)傳遞性

對于任何非null的引用值x、y、z。如果x.equals(y)==true,y.equals(z)==true,那麼x.equals(z)==true。

4) 一緻性

對于任何非null的引用值x和y,隻要equals的比較操作在對象所用的資訊沒有被修改,那麼多次調用x.equals(y)就會一緻性地傳回true,或者一緻性的傳回false。

5)非空性

所有比較的對象都不能為空。

30、深拷貝和淺拷貝的差別

31、Integer類對int的優化

Java并發

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

1、什麼是線程池,如何使用?為什麼要使用線程池?

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

2、Java中的線程池共有幾種?

Java有四種線程池:

第一種:newCachedThreadPool

不固定線程數量,且支援最大為Integer.MAX_VALUE的線程數量:

public static ExecutorService newCachedThreadPool() {
    // 這個線程池corePoolSize為0,maximumPoolSize為Integer.MAX_VALUE
    // 意思也就是說來一個任務就建立一個woker,回收時間是60s
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>());
}
複制代碼
           

可緩存線程池:

1、線程數無限制。 2、有空閑線程則複用空閑線程,若無空閑線程則建立線程。 3、一定程式減少頻繁建立/銷毀線程,減少系統開銷。

第二種:newFixedThreadPool

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

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

定長線程池:

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

第三種:newSingleThreadExecutor

可以了解為線程數量為1的FixedThreadPool:

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

單線程化的線程池:

1、有且僅有一個工作線程執行任務。 2、所有任務按照指定順序執行,即遵循隊列的入隊出隊規則。

第四種:newScheduledThreadPool。

支援定時以指定周期循環執行任務:

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

注意:前三種線程池是ThreadPoolExecutor不同配置的執行個體,最後一種是ScheduledThreadPoolExecutor的執行個體。

3、線程池原理?

從資料結構的角度來看,線程池主要使用了阻塞隊列(BlockingQueue)和HashSet集合構成。 從任務送出的流程角度來看,對于使用線程池的外部來說,線程池的機制是這樣的:

1、如果正在運作的線程數 < coreSize,馬上建立核心線程執行該task,不排隊等待;
2、如果正在運作的線程數 >= coreSize,把該task放入阻塞隊列;
3、如果隊列已滿 && 正在運作的線程數 < maximumPoolSize,建立新的非核心線程執行該task;
4、如果隊列已滿 && 正在運作的線程數 >= maximumPoolSize,線程池調用handler的reject方法拒絕本次送出。
複制代碼
           

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

線程池的線程複用:

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

  • firstTask:這是指定的第一個runnable可執行任務,它會在Woker這個工作線程中運作執行任務run。并且置空表示這個任務已經被執行。
  • getTask():這首先是一個死循環過程,工作線程循環直到能夠取出Runnable對象或逾時傳回,這裡的取的目标就是任務隊列workQueue,對應剛才入隊的操作,有入有出。

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

信号量

semaphore 可用于程序間同步也可用于同一個程序間的線程同步。

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

4、線程池都有哪幾種工作隊列?

1、ArrayBlockingQueue

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

2、LinkedBlockingQueue

一個基于連結清單結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。靜态工廠方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了這個隊列。

3、SynchronousQueue

一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀态,吞吐量通常要高于LinkedBlockingQueue,靜态工廠方法Executors.newCachedThreadPool使用了這個隊列。

4、PriorityBlockingQueue

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

5、怎麼了解無界隊列和有界隊列?

有界隊列

1.初始的poolSize < corePoolSize,送出的runnable任務,會直接做為new一個Thread的參數,立馬執行 。 2.當送出的任務數超過了corePoolSize,會将目前的runable送出到一個block queue中。 3.有界隊列滿了之後,如果poolSize < maximumPoolsize時,會嘗試new 一個Thread的進行救急處理,立馬執行對應的runnable任務。 4.如果3中也無法處理了,就會走到第四步執行reject操作。

無界隊列

與有界隊列相比,除非系統資源耗盡,否則無界的任務隊列不存在任務入隊失敗的情況。當有新的任務到來,系統的線程數小于corePoolSize時,則建立線程執行任務。當達到corePoolSize後,就不會繼續增加,若後續仍有新的任務加入,而沒有空閑的線程資源,則任務直接進入隊列等待。若任務建立和處理的速度差異很大,無界隊列會保持快速增長,直到耗盡系統記憶體。 當線程池的任務緩存隊列已滿并且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會采取任務拒絕政策。

6、多線程中的安全隊列一般通過什麼實作?

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

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

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

1、synchronized的原理?

synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實作的,Monitor 對象是同步的基本實作,而 synchronized 同步方法使用了ACC_SYNCHRONIZED通路标志來辨識一個方法是否聲明為同步方法,進而執行相應的同步調用。

在 Java 6 之前,Monitor 的實作完全是依靠作業系統内部的互斥鎖,因為需要進行使用者态到核心态的切換,是以同步操作是一個無差别的重量級操作。

現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不同的 Monitor 實作,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。

所謂鎖的更新、降級,就是 JVM 優化 synchronized 運作的機制,當 JVM 檢測到不同的競争狀況時,會自動切換到适合的鎖實作,這種切換就是鎖的更新、降級。

當沒有競争出現時,預設會使用偏斜鎖。JVM 會利用 CAS 操作,在對象頭上的 Mark Word 部分設定線程 ID,以表示這個對象偏向于目前線程,是以并不涉及真正的互斥鎖。這樣做的假設是基于在很多應用場景中,大部分對象生命周期中最多會被一個線程鎖定,使用偏斜鎖可以降低無競争開銷。

如果有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就需要撤銷(revoke)偏斜鎖,并切換到輕量級鎖實作。輕量級鎖依賴 CAS 操作 Mark Word 來試圖擷取鎖,如果重試成功,就使用普通的輕量級鎖;否則,進一步更新為重量級鎖(可能會先進行自旋鎖更新,如果失敗再嘗試重量級鎖更新)。

我注意到有的觀點認為 Java 不會進行鎖降級。實際上據我所知,鎖降級确實是會發生的,當 JVM 進入安全點(SafePoint)的時候,會檢查是否有閑置的 Monitor,然後試圖進行降級。

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

自旋鎖:

線程自旋說白了就是讓cpu在做無用功,比如:可以執行幾次for循環,可以執行幾條空的彙編指令,目的是占着CPU不放,等待擷取鎖的機會。如果旋的時間過長會影響整體性能,時間過短又達不到延遲阻塞的目的。

偏向鎖

偏向鎖就是一旦線程第一次獲得了監視對象,之後讓監視對象“偏向”這個線程,之後的多次調用則可以避免CAS操作,說白了就是置個變量,如果發現為true則無需再走各種加鎖/解鎖流程。

輕量級鎖:

輕量級鎖是由偏向所更新來的,偏向鎖運作在一個線程進入同步塊的情況下,當第二個線程加入鎖競争用的時候,偏向鎖就會更新為輕量級鎖;

重量級鎖

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

3、談談對Synchronized關鍵字涉及到的類鎖,方法鎖,重入鎖的了解?

synchronized修飾靜态方法擷取的是類鎖(類的位元組碼檔案對象)。

synchronized修飾普通方法或代碼塊擷取的是對象鎖。這種機制確定了同一時刻對于每一個類執行個體,其所有聲明為 synchronized 的成員函數中至多隻有一個處于可執行狀态,進而有效避免了類成員變量的通路沖突。

它倆是不沖突的,也就是說:擷取了類鎖的線程和擷取了對象鎖的線程是不沖突的!

public class Widget {

    // 鎖住了
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {

    // 鎖住了
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}
複制代碼
           

因為鎖的持有者是“線程”,而不是“調用”。

線程A已經是有了LoggingWidget執行個體對象的鎖了,當再需要的時候可以繼續**“開鎖”**進去的!

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

4、wait、sleep的差別和notify運作過程。

wait、sleep的差別

最大的不同是在等待時 wait 會釋放鎖,而 sleep 一直持有鎖。wait 通常被用于線程間互動,sleep 通常被用于暫停執行。

  • 首先,要記住這個差别,“sleep是Thread類的方法,wait是Object類中定義的方法”。盡管這兩個方法都會影響線程的執行行為,但是本質上是有差別的。
  • Thread.sleep不會導緻鎖行為的改變,如果目前線程是擁有鎖的,那麼Thread.sleep不會讓線程釋放鎖。如果能夠幫助你記憶的話,可以簡單認為和鎖相關的方法都定義在Object類中,是以調用Thread.sleep是不會影響鎖的相關行為。
  • Thread.sleep和Object.wait都會暫停目前的線程,對于CPU資源來說,不管是哪種方式暫停的線程,都表示它暫時不再需要CPU的執行時間。OS會将執行時間配置設定給其它線程。差別是,調用wait後,需要别的線程執行notify/notifyAll才能夠重新獲得CPU執行時間。
  • 線程的狀态參考 Thread.State的定義。新建立的但是沒有執行(還沒有調用start())的線程處于“就緒”,或者說Thread.State.NEW狀态。
  • Thread.State.BLOCKED(阻塞)表示線程正在擷取鎖時,因為鎖不能擷取到而被迫暫停執行下面的指令,一直等到這個鎖被别的線程釋放。BLOCKED狀态下線程,OS排程機制需要決定下一個能夠擷取鎖的線程是哪個,這種情況下,就是産生鎖的争用,無論如何這都是很耗時的操作。

notify運作過程

當線程A(消費者)調用wait()方法後,線程A讓出鎖,自己進入等待狀态,同時加入鎖對象的等待隊列。 線程B(生産者)擷取鎖後,調用notify方法通知鎖對象的等待隊列,使得線程A從等待隊列進入阻塞隊列。 線程A進入阻塞隊列後,直至線程B釋放鎖後,線程A競争得到鎖繼續從wait()方法後執行。

5、synchronized關鍵字和Lock的差別你知道嗎?為什麼Lock的性能好一些?

類别 synchronized Lock(底層實作主要是Volatile + CAS)
存在層次 Java的關鍵字,在jvm層面上 是一個類
鎖的釋放 1、已擷取鎖的線程執行完同步代碼,釋放鎖 2、線程執行發生異常,jvm會讓線程釋放鎖。 在finally中必須釋放鎖,不然容易造成線程死鎖。
鎖的擷取 假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待。 分情況而定,Lock有多個鎖擷取的方式,大緻就是可以嘗試獲得鎖,線程可以不用一直等待
鎖狀态 無法判斷 可以判斷
鎖類型 可重入 不可中斷 非公平 可重入 可判斷 可公平(兩者皆可)
性能 少量同步 大量同步

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

6、volatile原理。

在《Java并發程式設計:核心理論》一文中,我們已經提到可見性、有序性及原子性問題,通常情況下我們可以通過Synchronized關鍵字來解決這些個問題,不過如果對Synchonized原理有了解的話,應該知道Synchronized是一個較重量級的操作,對系統的性能有比較大的影響,是以如果有其他解決方案,我們通常都避免使用Synchronized來解決問題。

而volatile關鍵字就是Java中提供的另一種解決可見性有序性問題的方案。對于原子性,需要強調一點,也是大家容易誤解的一點:對volatile變量的單次讀/寫操作可保證原子性的,如long和double類型變量,但是并不能保證i++這種操作的原子性,因為本質上i++是讀、寫兩次操作。

volatile也是互斥同步的一種實作,不過它非常的輕量級。

volatile 的意義?

  • 防止CPU指令重排序

volatile有兩條關鍵的語義:

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

禁止進行指令重排序

要了解volatile關鍵字,我們得先從Java的線程模型開始說起。如圖所示:

2020年中進階Android面試秘籍(Java篇)前言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);
}
複制代碼
           
2020年中進階Android面試秘籍(Java篇)前言Java面試題

這段代碼啟動了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單例。

它的位元組碼如下:

2020年中進階Android面試秘籍(Java篇)前言Java面試題

可以看到被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、線程的生命周期

線程狀态流程圖

2020年中進階Android面試秘籍(Java篇)前言Java面試題
  • 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基本構成

2020年中進階Android面試秘籍(Java篇)前言Java面試題

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

1.類加載器(ClassLoader):在JVM啟動時或者在類運作将需要的class加載到JVM中。(下圖表示了從java源檔案到JVM的整個過程,可配合了解。

2020年中進階Android面試秘籍(Java篇)前言Java面試題

2.執行引擎:負責執行class檔案中包含的位元組碼指令;

3.記憶體區(也叫運作時資料區):是在JVM運作的時候操作所配置設定的記憶體區。運作時記憶體區主要可以劃分為5個區域,如圖:

2020年中進階Android面試秘籍(Java篇)前言Java面試題

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

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

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

2020年中進階Android面試秘籍(Java篇)前言Java面試題

雙親委派模型的整個工作流程非常的簡單,如下所示:

如果一個類加載器收到了加載類的請求,它不會自己立去加載類,它會先去請求父類加載器,每個層次的類加器都是如此。層層傳遞,直到傳遞到最高層的類加載器隻有當 父類加載器回報自己無法加載這個類,才會有當子類加載器去加載該類。

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、對象建立、記憶體布局,通路定位等。

轉載:https://juejin.im/post/5e5c5c52f265da575f4e7558