Android面試之Java基礎
-
-
- 掃碼加入交流群,擷取面試經驗,詳細腦圖檔案
- 寫在前面的話
- 思維導圖便于記憶
- 1. 基本資料類型
- 2.引用類型String
-
-
- String ,Stringbuffer ,Stringbulider的差別?
- String ,Stringbuffer ,Stringbulider效率誰快,為什麼?
-
- 3.int和Integer的差別?
- 4. 面向對象
-
-
- 1. 定義
- 2. 三大特征
-
- 5.接口和抽象類的差別?
- 6.final ,finally ,fianlize的差別?
- 7. 靜态變量和成員變量的不同?
- 8. 集合
-
-
- 1. 集合架構,list,map,set都有哪些具體的實作類,差別都是什麼?
- 2. 線程安全集合類與非線程安全集合類
- 3. HashMap
- 通過indexFor算法計算真實索引,确定在桶内的位置key
- 在并發過程中還是會出現死循環
- ConcurrentHashMap代替HashMap
-
- 9. 注解
- 10. 反射
- 11. 泛型
- 12. 記憶體區域
- 13. 描述一下GC的原理和回收政策
- 14. 類加載器
-
-
- 1. java類加載過程
-
- 15. 線程和并發
-
- 1. Synchronized、volatile、Lock(ReentrantLock)相關
-
- volatile
- synchronized
- Lock
- 2. 鎖
- 3. 線程
-
-
- 建立方式
- 生命周期
-
- 4. 線程池
- 寫在最後的話
- 掃碼加入交流群,擷取面試經驗,詳細腦圖檔案
-
掃碼加入交流群,擷取面試經驗,詳細腦圖檔案
寫在前面的話
Android 基于Java語言開發,是以Java知識也是Android開發人員 必不可少的知識,經過這次面試總結了一些知識點,有點散。是以這篇記錄一下總結的Java基礎知識。
思維導圖便于記憶
1. 基本資料類型
- 整形:byte(1個位元組), short(2個位元組) , int (4個位元組), long(8個位元組) 。
- 浮點型:float(4個位元組),double(8個位元組)。
- 浮點型:char(2個位元組) 。
- 布爾型:boolean (1個位元組)
2.引用類型String
String ,Stringbuffer ,Stringbulider的差別?
- String底層是一個final類型的字元數組,是以String的值是不可變的,每次對String的操作都會生成新的String對象,造成記憶體浪費。
- 在記憶體中專門有一個字元串緩沖池來存放字元串,當池中沒有對應的字元串就會建立一個新的字元串對象
- newString():建立字元串是:當字元串常量池中沒有會在池中建立一個字元串常量,在記憶體的堆中也開辟一個對象記憶體,當緩沖池有時,不會在被建立,隻會在記憶體中建立一個新的字元串對象。
- StringBuffer和StringBuilder都繼承AbstractStringBuilder抽象類,底層是可變的字元數組。
- StringBuffer是在jdk1.0出現的,是線程安全的,檢視源碼有synchronized關鍵字修飾,但是執行速度慢 ,StringBuilder是jdk1.5出現的,是非線程安全的,執行速度快。
String ,Stringbuffer ,Stringbulider效率誰快,為什麼?
在串行的的情況下,String 的效率最低(因為底層是不可變數組,每次操作都是建立一個Sting對象),Stringbuffer因為是同步,有syncchronzed稀釋,效率相對低,StringBulider效率最高。
3.int和Integer的差別?
- Integer是int的包裝類,int則是java的一種基本資料類型
- Integer變量必須執行個體化後才能使用,而int變量不需要
- Integer實際是對象的引用,當new一個Integer時,實際上是生成一個指針指向此對象;而int則是直接存儲資料值
- Integer的預設值是null,int的預設值是0
4. 面向對象
1. 定義
- 面向對象時基于面向過程的,面向過程強調的時步驟,注重的時過程,面向對象則注重于結果。
- 面向對象是一種思維方式,能簡化程式員開發。
- 萬物皆對象。
2. 三大特征
-
繼承
一個類的屬性和行為均于現有類相似,屬于現有類的一種,可以定義為現有類的子類,如果由多個類有相同的屬性和行為,可以抽取公共内容定義為父類
-
多态
表現多種形态,具有多種實作方式,比如一個人,可以是父親,兒子,老闆等,多種形态
-
封裝
将對象的屬性和行為封裝起來,類就是面向對象的封裝。
5.接口和抽象類的差別?
- 接口
- 隻有抽象方法
- 變量必須用public static final 修飾。
- 不能含有靜态代碼塊和靜态方法。
- 可以實作多個接口。
- 抽象類
- 可以提供成員方法。
- 成員變量可任意是各種類型。
- 可以有靜态代碼塊和靜态方法。
- 隻能單繼承。
6.final ,finally ,fianlize的差別?
- final是一個關鍵字,用來修飾類,變量,方法.修飾的類不能被繼承,但是可以繼承其他的類,修飾的方法不能被子類沖重寫,修飾的變量是常量,隻能被指派一次。
- finally是try-catch-finally語句的一個子產品,正常情況下裡面的代碼永遠會被執行,一般用來釋放資源。
- finalize是Object類中的方法,當對象變成垃圾的時候,由GC來調用finalize()方法回收。
7. 靜态變量和成員變量的不同?
① 所屬範圍不同。靜态變量是屬于類範圍的;成員變量是屬于對象範圍的。
② 存活時間不同。類的一生有着靜态變量的伴随;而成員變量隻能陪類走一程,對象産生的時候它就産生,而且它會随着對象的消亡而消亡。
③ 存儲位置不同。靜态變量時存儲在方法區裡的靜态區;成員變量存儲在堆棧記憶體區。
④ 調用方式不同。靜态變量可以通過類名調用,也可以通過對象來調用;成員變量隻能通過對象名調用。
8. 集合
需要檢視更多請檢視這裡
1. 集合架構,list,map,set都有哪些具體的實作類,差別都是什麼?
- List:有序、可重複;索引查詢速度快;插入、删除伴随資料移動,速度慢;
- Set:無序,不可重複;
- Map:鍵值對,鍵唯一,值多個;
- List,Set都是繼承自Collection接口,Map則不是;
Java集合的類結構圖如下所示:
2. 線程安全集合類與非線程安全集合類
- LinkedList、ArrayList、HashSet是非線程安全的,Vector是線程安全的;
- HashMap是非線程安全的,HashTable是線程安全的;
- StringBuilder是非線程安全的,StringBuffer是線程安的。
3. HashMap
- jdk1.7,底層采用了數組+連結清單,頭插法;
- 負載因子:預設容量為16,負載因子為0.75,當超過16*0.75=12時,就要僅從hashmap的擴容,擴容涉及到rehash和複制資料等,會非常消耗性能。
- 真正存儲資料的是entry<key,value>[] table,entry是hashmap的一個靜态内部類,有key,value,next,hash(key的hashcode)成員變量。
- put 過程
- 判斷目前數組是否要初始化。
- 如果key為空,則put一個空值進去。
- 根據key計算出hashcode。
- 根據hsahcode定位出在桶内的位置。
- 如果桶是連結清單,則需要周遊判斷hashcode ,如果key和原來的key是否相等,相等則進行覆寫,傳回原來的值。
- 如果桶是空的,說明目前位置沒有資料存入,新增一個 Entry 對象寫入目前位置.當調用 addEntry 寫入 Entry 時需要判斷是否需要擴容。如果需要就進行兩倍擴充,并将目前的 key 重新 hash 并定位。而在 createEntry中會将目前位置的桶傳入到建立的桶中,如果目前桶有值就會在位置形成連結清單。)。
- get過程
- 根據key計算出hashcode,并定位到桶内的位置。
- 判斷是不是連結清單,如果是,則需要根據周遊直到 key 及 hashcode 相等時候就傳回值,如果不是就根據 key、key 的 hashcode 是否相等來傳回值。
- 如果啥也沒取到就傳回null。
- 1.8,采用是的底層+連結清單+紅黑樹,尾插法,增加一個門檻值進行判斷是否将連結清單轉紅黑樹,HashEntry 修改為 Node.解決hash沖突,造成連結清單越來越長,查詢慢的問題。
- put 過程
- 判斷目前桶是不是空,空就需要初始化;
- 根據key,計算出hashcode,根據hashcode,定位到具體的桶中,并判斷目前桶是不是為空,為空表明沒有hsah沖突建立一個新桶即可;
- 如果有hash沖突,那麼就要比較目前桶中的 key、key 的 hashcode 與寫入的 key 是否相等,相等就指派給 e,在第 8 步的時候會統一進行指派及傳回;
- 如果目前位置是紅黑樹,就按照紅黑樹的方式寫入資料;
- 如果目前位置是連結清單,則需要把key,value封裝一個新的節點,添加到目前的桶後面(尾插法),形成連結清單;
- 接着判斷目前連結清單的大小是否大于預設的門檻值,大于時就要轉換為紅黑樹;
- 如果在周遊過程中找到 key 相同時直接退出周遊;
- 如果 e != null 就相當于存在相同的 key,那就需要将值覆寫;
- 最後判斷是否需要進行擴容;
- get過程
- 首先将 key hash 之後取得所定位的桶。
- 如果桶為空則直接傳回 null 。
- 否則判斷桶的第一個位置(有可能是連結清單、紅黑樹)的 key 是否為查詢的 key,是就直接傳回 value。
- 如果第一個不比對,則判斷它的下一個是紅黑樹還是連結清單。
- 紅黑樹就按照樹的查找方式傳回值。
- 不然就按照連結清單的方式周遊比對傳回值。
通過indexFor算法計算真實索引,确定在桶内的位置key
static int indexFor(int h, int length) {
return h & (length-1);
}
- 使用了位運算來得到索引
- HashMap的初始容量和擴容都是以2的次方來進行的,如果不滿足則會出現key沖突的情況
在并發過程中還是會出現死循環
在 HashMap 擴容的時候會調用 resize() 方法,就是這裡的并發操作容易在一個桶上形成環形連結清單;這樣當擷取一個不存在的 key 時,計算出的 index 正好是環形連結清單的下标就會出現死循環:在 1.7 中 hash 沖突采用的頭插法形成的連結清單,在并發條件下會形成循環連結清單,一旦有查詢落到了這個連結清單上,當擷取不到值時就會死循環。
ConcurrentHashMap代替HashMap
ConcurrentHashMap是線程安全的。
原理
- jdk1.7:使用鎖分段技術,初始16個segment,采用ReentrantLock,樂觀鎖,嘗試擷取鎖失敗,會不斷嘗試,當達到門檻值時,會阻塞等待,真正存放資料的地方時hashEntry;
- jdk1.8:采用CAS和synchronized來保證線程同步,get的時候不加鎖,使用volitaile來保證變量的原子性,其他和hashmap一樣;
9. 注解
注解相當于一種标記,在程式中加了注解就等于為程式打上了某種标記。程式可以利用ava的反射機制來了解你的類及各種元素上有無何種标記,針對不同的标記,就去做相應的事件。标記可以加在包,類,字段,方法,方法的參數以及局部變量上。
10. 反射
Java 中的反射首先是能夠擷取到Java中要反射類的位元組碼, 擷取位元組碼有三種方法:
- Class.forName(className)
- 類名.class
- this.getClass()。
然後将位元組碼中的方法,變量,構造函數等映射成相應的Method、Filed、Constructor等類,這些類提供了豐富的方法可以被我們所使用。
11. 泛型
泛型是泛指某種特定的資料類型,是jdk1.5的新特性。
好處
- 提高了安全性(不會出現類型轉換異常);
- 将運作時期會可能出現的異常轉移至編譯期;
- 優化程式設計(定義為泛型後我們的程式可以放多種資料類型的一種) ;
- 避免來強制類型轉換的麻煩;
12. 記憶體區域
運作時記憶體區域如圖所示:
- 程式計數器
- 當線程切換時,用來标記目前線程所執行的位元組碼的行号訓示器;
- 每一個線程都有一個獨立的程式計數器,用來線程切換時能恢複到正确的位置;
- JAVA虛拟機棧
- 線程私有,生命周期和線程一樣,用來存儲:局部變量表,操作數棧,動态連結,方法出口等;
- 局部變量表所占用的記憶體在編譯器就完成配置設定,當進入一個方法時,這個方法需要在幀中配置設定多大的局部變量空間是完全确定的,在方法運作期間不會改變局部變量表的大小;
- 本地方法棧
- 為虛拟機執行native方法;
- JAVA堆
- 存放對象執行個體;
- java堆是垃圾收集器管理的主要區域;
- 被所有線程共享的一塊記憶體區域;
- 方法區
- 各個線程所共享的記憶體區域;
- 用于存儲class二進制檔案,包含了虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料;
- 線程安全的;
- 運作時常量池存在方法區中;
- 常量池
- 常量是指被final修飾的變量,值一旦确定就無法改變;
- 常量池在編譯期間就将一部分資料存放于該區域,包含基本資料類型如int、long等以final聲明的常量值,和String字元串、特别注意的是對于方法運作期位于棧中的局部變量String常量的值可以通過 String.intern()方法将該值置入到常量池中。
- 分類:
- Class檔案常量池;
- 運作時常量池;
- 全局字元串常量池;
- 基本類型包裝類對象常量池;
13. 描述一下GC的原理和回收政策
了解更多請點選這裡
- 對象存活判定算法
- 引用計數法
- 用到就+1,不用就-1,直到為0 ,就證明類不可能被調用
- 但是在java中沒有使用,很難解決對象互相引用
-
可達性分析法
思想:以“GC roots”作為起始點,從這些節點向下搜尋,搜尋所走過的路徑稱為引用鍊,當一個對象到GC Roots沒有任何引用鍊(即GC Roots到對象不可達)時,則證明此對象是不可用的。
- 引用計數法
- GC roots 來源
- 虛拟機棧中引用的對象。
- 方法區中類靜态屬性引用的對象。
- 方法區中常量引用的對象。
- 本地方法棧中JNI引用的對象
- 四種引用對象
強引用:不進行垃圾回收
軟引用:在記憶體溢出前進行垃圾回收
弱引用:隻要有垃圾回收就會回收掉
虛引用:無用對象,垃圾收集時會接收到通知
- 對于可達性分析算法而言,未到達的對象并非是“非死不可”的,若要宣判一個對象死亡,至少需要經曆兩次标記階段;
- 第一次标記:如果沒有和gcroots 相連,就進行第一次标記篩選,篩選條件為是否有必要執行該對象的finalize方法,判斷對象有沒有覆寫finalize 方法,或者有沒有執行過,執行過則代表要進行回收,如果覆寫了并沒有執行過,則會放到F-Queue的隊列中,進行第二次标記;
- 第二次标記對:F-Queue中對象進行第二次标記,如果在finalize方法中關聯上了gcroots的引用連,則不會被回收,從即将回收的集合裡移除;
- 方法區的垃圾回收
- 廢棄常量
- 無用的類
- 該類的所有執行個體都已經被回收,即Java堆中不存在該類的任何執行個體。
- 加載該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。
- 回收算法
- 複制算法:
- 複制算法将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這種算法适用于對象存活率低的場景,比如新生代。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況。
- 标記-清除:
- 标記-清除算法采用從根集合進行掃描,對存活的對象進行标記,标記完畢後,再掃描整個空間中未被标記的對象,進行回收。标記-清除算法不需要進行對象的移動,并且僅對不存活的對象進行處理,在存活對象比較多的情況下極為高效,但由于标記-清除算法直接回收不存活的對象,是以會造成記憶體碎片。
- 标記-整理:
- 标記-整理算法采用标記-清除算法一樣的方式進行對象的标記,但在清除時不同,在回收不存活的對象占用的空間後,會将所有的存活對象往左端空閑空間移動,并更新對應的指針。标記-整理算法是在标記-清除算法的基礎上,又進行了對象的移動,是以成本更高,但是卻解決了記憶體碎片的問題。該垃圾回收算法适用于對象存活率高的場景(老年代)。
- 分代收集算法:
- 不同的對象的生命周期(存活情況)是不一樣的,而不同生命周期的對象位于堆中不同的區域,是以對堆記憶體不同區域采用不同的政策進行回收可以提高 JVM 的執行效率。當代商用虛拟機使用的都是分代收集算法:新生代對象存活率低,就采用複制算法;老年代存活率高,就用标記清除算法或者标記整理算法
- 新生代:
- 老年代:
- 複制算法:
- 垃圾收集器
-
Serial收集器(複制算法):
新生代單線程收集器,标記和清理都是單線程,優點是簡單高效;
-
**ParNew收集器 (複制算法): **
新生代收并行集器,實際上是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現;
-
Parallel Scavenge收集器 (複制算法):
新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 使用者線程時間/(使用者線程時間+GC線程時間),高吞吐量可以高效率的利用CPU時間,盡快完成程式的運算任務,适合背景應用等對互動相應要求不高的場景
-
**Serial Old收集器 (标記-整理算法): **
老年代單線程收集器,Serial收集器的老年代版本
-
CMS(Concurrent Mark Sweep)收集器(标記-清除算法):
老年代并行收集器,以擷取最短回收停頓時間為目标的收集器,具有高并發、低停頓的特點,追求最短GC回收停頓時間。
-
Parallel Old收集器 (标記-整理算法):
老年代并行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;
-
G1(Garbage First)收集器 (标記-整理算法):
Java堆并行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基于“标記-整理”算法實作,也就是說不會産生記憶體碎片。此外,G1收集器不同于之前的收集器的一個重要特點是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限于新生代或老年代。
-
-
記憶體配置設定與回收政策
JAVA自動記憶體管理:給對象配置設定記憶體 以及 回收配置設定給對象的記憶體。
- 對象優先在Eden配置設定,當Eden區沒有足夠空間進行配置設定時,虛拟機将發起一次MinorGC。
- 大對象直接進入老年代。如很長的字元串以及數組。很長的字元串以及數組。
- 長期存活的對象将進入老年代。當對象在新生代中經曆過一定次數(預設為15)的Minor GC後,就會被晉升到老年代中。
- 動态對象年齡判定。為了更好地适應不同程式的記憶體狀況,虛拟機并不是永遠地要求對象年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
14. 類加載器
1. java類加載過程
-
加載
1. 通過一個類的全限定名來擷取定義此類的二進制位元組流
2. 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構
3. 将類的class檔案讀入記憶體,并為之建立一個java.lang.Class對象
4. 當程式中使用任何類時,系統都會為之建立一個java.lang.Class對象
- 二進制資料的來源 :
從本地檔案系統加載class檔案;
從JAR包加載class檔案;
通過網絡加載class檔案
把一個Java源檔案動态編譯,并執行加載
- 二進制資料的來源 :
- 連接配接
-
驗證:
驗證階段用于檢驗被加載的類是否有正确的内部結構,并和其他類協調一緻
- 驗證方式:
檔案格式驗證:主要驗證位元組流是否符合Class檔案格式規範,并且能被目前的虛拟機加載處理
中繼資料驗證:對位元組碼描述的資訊進行語義的分析,分析是否符合java的語言文法的規範。
位元組碼驗證:最重要的驗證環節,分析資料流和控制,确定語義是合法的,符合邏輯的。主要的針對中繼資料驗證後對方法體的驗證。保證類方法在運作時不會有危害出現
符号引用驗證:主要是針對符号引用轉換為直接引用的時候,是會延伸到第三解析階段,主要去确定通路類型等涉及到引用的情況,主要是要保證引用一定會被通路到,不會出現類等無法通路的問題。
-
準備:
類準備階段負責為類的靜态變量配置設定記憶體,并設定預設初始值;
-
解析:
将類的二進制資料中的符号引用替換成直接引用;
-
-
初始化
1. 主要是對類變量進行初始化;
2. 聲明類變量時指定初始值;
3. 使用靜态初始化塊為類變量指定初始值;
- 類加載器
- 啟動類加載器
- 擴充類加載器
- 應用類加載器
- 類加載機制
-
全盤負責
所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴和引用其他Class也将由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。
-
雙親委派
工作原理
先讓父類加載器試圖加載該Class,隻有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。
優勢
1. 采用雙親委派模式的是好處是Java類随着它的類加載器一起具備了一種帶有優先級的層次關系,通過這種層級關可以避免類的重複加載。
2. 其次是考慮到安全因素,java核心api中定義類型不會被随意替換。
-
緩存機制
緩存機制将會保證所有加載過的Class都會被緩存,當程式中需要使用某個Class時,類加載器先從緩存區中搜尋該Class,隻有當緩存區中不存在該Class對象時,系統才會讀取該類對應的二進制資料,并将其轉換成Class對象,存入緩沖區中。
-
15. 線程和并發
1. Synchronized、volatile、Lock(ReentrantLock)相關
volatile
1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序。
指令重排序:一般來說,處理器為了提高程式運作效率,可能會對輸入代碼進行優化,它不保證程式中各個語句的執行先後順序同代碼中的順序一緻,但是它會保證程式最終執行結果和代碼順序執行的結果是一緻的。
- volatile沒辦法保證對變量的操作的原子性
1)當程式執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
2)在進行指令優化時,不能将在對volatile變量通路的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。
-
volatile的原理和實作機制
加入volatile關鍵字時,會多出一個lock字首指令,lock字首指令實際上相當于一個記憶體屏障(也稱記憶體栅欄),記憶體屏障會提供3個功能:
1)它確定指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制将對緩存的修改操作立即寫入主存;
3)如果是寫操作,它會導緻其他CPU中對應的緩存行無效。
- 使用volatile必須具備以下2個條件【保證原子性】:
1)對變量的寫操作不依賴于目前值
2)該變量沒有包含在具有其他變量的不變式中
在使用單例的時候用過,保證單例對象在多線程可見
synchronized
-
synchronized可作用于一段代碼或方法,既可以保證可見性,又能夠保證原子性。
(1)可見性展現在:通過synchronized或者Lock能保證同一時刻隻有一個線程擷取鎖然後執行同步代碼,并且在釋放鎖之前會将對變量的修改重新整理到主存中。
(2)原子性表現在:要麼不執行,要麼執行到底。
(3)性能方面,synchronized關鍵字是防止多個線程同時執行一段代碼,就會影響程式執行效率,而volatile關鍵字在某些情況下性能要優于synchronized。
(*)但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字無法保證操作的原子性。
-
synchronized的缺陷
1)如果一個代碼塊被synchronized修飾了,當一個線程擷取了對應的鎖,并執行該代碼塊時,其他線程便隻能一直等待,等待擷取鎖的線程釋放鎖,而這裡擷取鎖的線程釋放鎖隻會有兩種情況:
1)擷取鎖的線程執行完了該代碼塊,然後線程釋放對鎖的占有;
2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。
3)如果這個擷取鎖的線程由于要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便隻能等待,影響程式執行效率。
(2)當多個線程都是要讀一個資料的時候,synchronized效率很低。
- synchronized的三種應用方式
-
修飾普通方法
一個對象中的加鎖方法隻允許一個線程通路。但要注意這種情況下鎖的是通路該方法的執行個體對象, 如果多個線程不同對象通路該方法,則無法保證同步。
-
修飾靜态方法
由于靜态方法是類方法, 是以這種情況下鎖的是包含這個方法的類,也就是類對象;這樣如果多個線程不同對象通路該靜态方法,也是可以保證同步的。
-
修飾代碼塊
其中普通代碼塊 如Synchronized(obj) 這裡的obj 可以為類中的一個屬性、也可以是目前的對象,它的同步效果和修飾普通方法一樣;Synchronized方法 (obj.class)靜态代碼塊它的同步效果和修飾靜态方法類似。
Lock
- Lock是一個接口,可以實作同步通路。
- ReentrantLock,意思是“可重入鎖”,是唯一實作了Lock接口的類,并且ReentrantLock提供了更多的方法。
- 如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性實際上表明了鎖的配置設定機制:基于線程的配置設定,而不是基于方法調用的配置設定。
舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。
- volatile 與 synchronized對比
1)volatile本質是在告訴jvm目前變量在寄存器中的值是不确定的,需要從主存中讀取,synchronized則是鎖定目前變量,隻有目前線程可以通路該變量,其他線程被阻塞住.
(2)volatile僅能使用在變量級别,synchronized則可以使用在變量,方法.
(3)volatile僅能實作變量的修改可見性,而synchronized則可以保證變量的修改可見性和原子性.
(4)volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞.
(5)當一個域的值依賴于它之前的值時,volatile就無法工作了,如n=n+1,n++等。如果某個域的值受到其他域的值的限制,那麼volatile也無法工作,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。
(6)使用volatile而不是synchronized的唯一安全的情況是類中隻有一個可變的域。
- synchronized和Lock差別
(1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是内置的語言實作;
(2)synchronized在發生異常時,會自動釋放線程占有的鎖,是以不會導緻死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,是以使用Lock時需要在finally塊中釋放鎖;
(3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
(4)synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之後,系統會自動讓線程釋放對鎖的占用;而Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導緻出現死鎖現象。
(5)通過Lock可以知道有沒有成功擷取鎖,而synchronized卻無法辦到。 (6)Lock可以提高多個線程進行讀操作的效率。
(7)在性能上來說,如果競争資源不激烈,兩者的性能是差不多的,而當競争資源非常激烈時(即有大量線程同時競争),此時Lock的性能要遠遠優于synchronized。是以說,在具體使用時要根據适當情況選擇。
2. 鎖
-
自旋鎖
占着CPU不妨,等待擷取鎖的機會
-
偏向鎖
偏向鎖就是一旦線程第一次獲得了監視對象,之後讓監視對象“偏向”這個線程,之後的多次調用則可以避免
CAS操作
-
輕量級鎖
偏向鎖更新而來,偏向鎖運作在一個線程進入同步塊的情況下,當第二個線程加入鎖競争用的時候,偏向鎖就會更新為輕量級鎖
- 重量級鎖
- 對象螢幕
- 既要做到互斥,又要做到線程同步
3. 線程
建立方式
- 繼承Thread類
-
建立方法
1. 定義threade的子類,重寫run方法
2. 建立Thread子類的執行個體,即建立了線程對象
3. 調用線程對象的start()方法來啟動該線程
- 優缺點
- 優點:編寫簡單,如果需要通路目前線程,無需使用Thread.currentThread()方法,直接使用this,即可獲得目前線程
- 缺點:因為線程類已經繼承了Thread類,是以不能再繼承其他的父類
- 實作Runnable接口
- 建立方法
- 定義runnable接口的實作類,并重寫該接口的run()方法
- 建立 Runnable實作類的執行個體,并依此執行個體作為Thread的target來建立Thread對象,該Thread對象才是真正的線程對象。
-
調用線程對象的start()方法來啟動該線程
實作代碼如圖:
- 優缺點
- 優點:線程類隻是實作了Runable接口,還可以繼承其他的類
- 缺點:程式設計稍微複雜,如果需要通路目前線程,必須使用Thread.currentThread()方法
- 實作Callable接口
- 建立方式
- 建立Callable接口的實作類,并實作call()方法,該call()方法将作為線程執行體,并且有傳回值
- 建立Callable實作類的執行個體,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的傳回值。
- 使用FutureTask對象作為Thread對象的target建立并啟動新線程
-
調用FutureTask對象的get()方法來獲得子線程執行結束後的傳回值
實作代碼如圖所示:
- Runnable和Callable的差別:
- Runnable實作是run方法,callable 重寫的是call(),并有傳回值。
- call方法可以抛異常,run方法不行。
- 運作Callable任務可以拿到一個Future對象,表示異步計算的結果.它提供了檢查計算是否完成的方法,以等待計算的完成,并檢索計算的結果。通過Future對象可以了解任務執行情況,可取消任務的執行,還可擷取執行結果。
生命周期
-
建立
就是剛使用new方法,new出來的線程;
就緒:
就是調用的線程的start()方法後,這時候線程處于等待CPU配置設定資源階段,誰先搶的CPU資源,誰開始執行
-
運作:
當就緒的線程被排程并獲得CPU資源時,便進入運作狀态,run方法定義了線程的操作和功能;
-
阻塞:
在運作狀态的時候,可能因為某些原因導緻運作狀态的線程變成了阻塞狀态,比如sleep()、wait()之後線程就處于了阻塞狀态,這個時候需要其他機制将處于阻塞狀态的線程喚醒,比如調用notify或者notifyAll()方法。喚醒的線程不會立刻執行run方法,它們要再次等待CPU配置設定資源進入運作狀态;
-
死亡:
如果線程正常執行完畢後或線程被提前強制性的終止或出現異常導緻結束,那麼線程就要被銷毀,釋放資源;
- notify、notifyAll()的差別?
notify() 方法随機喚醒對象的等待池中的一個線程,進入鎖池;notifyAll() 喚醒對象的等待池中的所有線程,進入鎖池。
- sleep()、wait()的差別?
sleep()
方法是線程類(Thread)的靜态方法,讓調用線程進入睡眠狀态,讓出執行機會給其他線程,等到休眠時間結束後,線程進入就緒狀态和其他線程一起競争cpu的執行時間。因為sleep() 是static靜态的方法,他不能改變對象的機鎖,當一個synchronized塊中調用了sleep()方法,線程雖然進入休眠,但是對象的機鎖沒有被釋放,其他線程依然無法通路這個對象。
wait()
是Object類的方法,當一個線程執行到wait方法時,它就進入到一個和該對象相關的等待池,同時釋放對象的機鎖,使得其他線程能夠通路,可以通過notify,notifyAll方法來喚醒等待的線程。
sleep() 和 wait() 的差別就是: 調用sleep方法的線程不會釋放對象鎖,而調用wait() 方法會釋放對象鎖 線程池
4. 線程池
-
使用線程池的好處
1.重用,減少建立和銷毀所産生的性能開銷
2.避免過多的線程搶占CPU而産生阻塞現象
3.統一管理
-
常見的線程池
1. CacheThreadPool(),可緩存的線程池。
1、線程數無限制。
2、有空閑線程則複用空閑線程,若無空閑線程則建立線程。
3、一定程式減少頻繁建立/銷毀線程,減少系統開銷。
- FixedThreadPool(n)定長線程池
1、可控制線程最大并發數(同時執行的線程數)。
2、超出的線程會在隊列中等待
3.SingleThreadExecutor()單線程化的線程池
1、有且僅有一個工作線程執行任務。
2、所有任務按照指定順序執行,即遵循隊列的入隊出隊規則。串行
4.ScheduledThreadPool(int n):建立一個定長線程池,支援定時及周期性任務執行
寫在最後的話
革命尚未成功,同志仍需努力。