談談對面向對象思想的理解
首先面向對象類似於找什么人做什么事,比如我們需要一個隨機數,就可以調用Random類,使用它的方法。與面向過程的編程思想不同,面向過程的編程思想跟注重解決問題所需要的步驟,該去如何設計,然后一步步的實現,面向對象的思維更多的是考慮如何去選擇合適的工具,然后組織到一起干一件事。
追問)面向對象的三大特征:封裝,繼承,多態
封裝:我們通過封裝,只為用戶提供接口,而隱藏了內部的具體實現。比如我們使用jdbc每次都需要進行注冊驅動,建立連接,創建SQL的語句,運行語句,處理運行結果,釋放資源以上六步,就可以對以上步驟進行封裝,就引入了mybatis,只需要調用其方法而不需要在意mybatis內部是怎么執行的。再比如,javabean的屬性私有,提供getset對外訪問,因為屬性的賦值或者獲取邏輯只能由javabean本身決定。而不能由外部胡亂修改。
private String name; public void setName(String name){ this.name = "xxx"+name; }
該name有自己的命名規則,明顯不能由外部直接賦值
繼承:繼承就是父類提取子類們共有的方法,而子類只需要着重於自己獨有的方法,優點也在於減少代碼冗余。
多態:首先多態的需要的三個條件繼承,方法重寫,父類引用指向子類對象,父類的引用指向的子類的不同就會有不同的實現。多態的弊端在於無法調到子類特有的方法。
父類類型 變量名 = new 子類對象 ; 變量名.方法名();
JDK JRE JVM
JDK:
Java Develpment Kit java 開發工具
JRE:
Java Runtime Environment java運行時環境 JVM:
java Virtual Machine java 虛擬機
因為每個操作系統都有自己的jvm所以就能做到一次編譯處處運行
==和equals比較
==對比的是棧中的值,基本數據類型是變量值,引用類型是堆中內存對象的地址
equals:object中默認也是采用==比較,通常會重寫,例如String
//不做處理的equals public boolean equals(Object obj) { return (this == obj); }
//可以看出,String類中被復寫的equals()方法其實是比較兩個字符串的內容。 public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
常見題目
public class StringDemo { public static void main(String args[]) { String str1 = "Hello"; String str2 = new String("Hello"); String str3 = str2; // 引用傳遞 System.out.println(str1 == str2); // false System.out.println(str1 == str3); // false System.out.println(str2 == str3); // true System.out.println(str1.equals(str2)); // true System.out.println(str1.equals(str3)); // true System.out.println(str2.equals(str3)); // true } }
方法的重寫和重載的區別
- 重載(overload):發生在一個類里面,方法名相同,參數列表不同(混淆點:跟返回類型沒關系
以下不構成重載
public double add(int a,int b)
public int add(int a,int b)
- 重寫(override):發生在父類子類之間的,方法名相同,參數列表相同。拋出的異常范圍小於 等於父類,訪問修飾符范圍大於等於父類;如果父類方法訪問修飾符為private則子類就不能重寫該方法。
final
- 修飾類:表示類不可被繼承
- 修飾方法:表示方法不可被子類覆蓋(重寫),但是可以重載
- 修飾變量:表示變量一旦被賦值就不可以更改它的值
(1) 修飾成員變量
- 如果final修飾的是類變量,只能在靜態初始化塊中指定初始值或者聲明該類變量時指定初始值。
- 如果final修飾的是成員變量,可以在非靜態初始化塊、聲明該變量或者構造器中執行初始值。
(2) 修飾局部變量
系統不會為局部變量進行初始化,局部變量必須由程序員顯示初始化。因此使用final修飾局部變量時,即可以在定義時指定默認值(后面的代碼不能對變量再賦值),也可以不指定默認值,而在后面的代碼中對final變量賦初值(僅一次)
public class FinalVar { final static int a = 0;//再聲明的時候就需要賦值或者靜態代碼塊賦值 /** * static{ * a = 0; } */ final int b = 0;//再聲明的時候就需要賦值或者代碼塊中賦值或者構造器賦值 /*{ b = 0; }*/ public static void main(String[] args) { final int localA; //局部變量只聲明沒有初始化,不會報錯,與final無關。 localA = 0;//在使用之前一定要賦值 // localA = 1; 但是不允許第二次賦值 } }
(3) 修飾基本類型數據和引用類型數據
- 如果是基本數據類型的變量,則其數值一旦在初始化之后便不能更改;
- 如果是引用類型的變量,則在對其初始化之后便不能再讓其指向另一個對象。但是引用的值是可變的*。
public class FinalReferenceTest { public static void main() { final int[] iArr = {1, 2, 3, 4}; iArr[2] = -3;//合法 iArr = null;//非法,對iArr不能重新賦值 final Person p = new Person(25); p.setAge(24);//合法 p = null;//非法 } }
為什么局部內部類和匿名內部類只能訪問局部final變量?
編譯之后會生成兩個class文件,Test.class Test1.class
public class Test { public static void main(String[] args) { } //局部final變量a,b public void test(final int b) {//jdk8在這里做了優化, 不用寫,語法糖,但實際上也是有的,也不能修改 } } final int a = 10; //匿名內部類 new Thread() { public void run(){ System.out.println(a); System.out.println(b); } ; }.start(); class OutClass { private int age = 12; public void outPrint(final int x) { class InClass { public void InPrint() { System.out.println(x); System.out.println(age); } } new InClass().InPrint(); } }
首先需要知道的一點是: 內部類和外部類是處於同一個級別的,內部類不會因為定義在方法中就會隨着方法的執行完畢就被銷毀。
這里就會產生問題:當外部類的方法結束時,局部變量就會被銷毀了,但是內部類對象可能還存在(只有沒有人再引用它時,才會死亡)。這里就出現了一個矛盾:內部類對象訪問了一個不存在的變量。為了解決這個問題,就將局部變量復制了一份作為內部類的成員變量,這樣當局部變量死亡后,內部類仍可以訪問它,實際訪問的是局部變量的"copy"。這樣就好像延長了局部變量的生命周期
將局部變量復制為內部類的成員變量時,必須保證這兩個變量是一樣的,也就是如果我們在內部類中修
改了成員變量,方法中的局部變量也得跟着改變,怎么解決問題呢?
就將局部變量設置為final,對它初始化后,我就不讓你再去修改這個變量,就保證了內部類的成員變量和方法的局部變量的一致性。這實際上也是一種妥協。使得局部變量與內部類內建立的拷貝保持一致。
String、StringBuffer、StringBuilder
- String是final修飾的,不可變,每次操作都會產生新的String對象
- StringBuffer和StringBuilder都是在原對象上操作 StringBuffer是線程安全的,StringBuilder線程不安全的 StringBuffer方法都是synchronized修飾的
性能:StringBuilder > StringBuffer > String
場景:經常需要改變字符串內容時使用后面兩個,優先使用StringBuilder,多線程使用共享變量時使用StringBuffer。
接口和抽象類的區別
- 抽象類可以存在普通成員函數,而接口中只能存在public abstract 方法
- 抽象類中的成員變量可以是各種類型的,而接口中的成員變量只能是public static final類型的
- 抽象類只能繼承一個,接口可以實現多個
接口的設計目的,是對類的行為進行約束,制定規則,也就是提供一種機制,可以強制要求不同的類具有相同的行為。它只約束了行為的有無,但不對如何實現行為進行限制。就好比接口IUserDao接口會定義對user的CRUD操作,所以他的實現類UserDaoimpl就必須對其進行實現。
而抽象類的設計目的,是代碼復用。當不同的類具有某些相同的行為,且其中一部分行為的實現方式一致時,就可以使用抽象類提取共有方法。
當你關注一個事物的本質的時候,用抽象類;當你關注一個操作的時候,用接口。
什么是向上轉型?向下轉型?
//向上轉型: Person person = new Student(); //安全的 //向下轉型: Teacher teacher = (Teacher)person; //不安全的
向上轉型,是多態。
向下轉型,為了防止編譯錯誤,需要用到instanceof
向下轉型需要記住,不是什么類都能強轉的。必須是父與子的關系。所以我們用來對所有想要強轉的類進行約束。
int和Integer的區別?什么是裝拆箱?
Integer i1 = new Integer(12); //自動拆箱 Integer i2 = new Integer(12); //自動拆箱 System.out.println(i1 == i2);//false Integer i3 = 126; //自動裝箱 Integer i4 = 126; //自動裝箱 int i5 = 126; System.out.println(i3 == i4);//true System.out.println(i3 == i5);//true Integer i6 = 128; Integer i7 = 128; int i8 = 128; System.out.println(i6 == i7);//false System.out.println(i6 == i8);//true
- 都定義為Integer的比較:
new:
一旦new,就是開辟一塊新內存,結果肯定是false
不new:
看范圍
Integer做了緩存,-128至127,當你取值在這個范圍的時候,會采用緩存的對象,所以會相等
當不在這個范圍,內部創建新的對象,此時不相等
- Integer和int的比較:
實際比較的是數值,Integer會做拆箱的動作,來跟基本數據類型做比較
此時跟是否在緩存范圍內或是否new都沒關系
手撕冒泡算法
public static int[] bubbleSort(int[] array){ if(array.length <= 1){ return array; } //重復n次冒泡 for(int i=0;i<array.length;i++){ //是否可以提交退出冒泡的標記 boolean flag = false; //相鄰之間兩兩比較,並且每次減少一位參與比較 for(int j=0;j<array.length-i-1;j++){ if(array[j] > array[j+1]){ //需要交換 int temp = array[j]; array[j] = array[j+1]; array[j+1] = temp; flag = true;//有數據交換,不能提前退出 } } if(!flag){ //沒有數據交換,提前退出冒泡比較 break; } } return array; }
List和Set的區別
- List:有序,按對象進入的順序保存對象,可重復,允許多個Null元素對象,可以使用Iterator取出所有元素,在逐一遍歷,還可以使用get(int index)獲取指定下標的元素
- Set:無序,不可重復,最多允許有一個Null元素對象,取元素時只能用Iterator接口取得所有元素,在逐一遍歷各個元素
HashSet的底層原理(hashCode與equals)
hashCode()是Object類的方法,返回一個int的整數的哈希碼。這個哈希碼的作用是確定該對象在哈希表中的索引位置。散列表存儲的是鍵值對(key-value),它的特點是:能根據“鍵”快速的檢索出對應的“值”。這其中就利用到了散列碼!(可以快速找到所需要的對象)效率極高。
HashSet:對象加入HashSet時,HashSet會先計算對象的hashcode值來判斷對象加入的位置,看該位置是否有值,如果沒有,HashSet會假設對象沒有重復出現。但是如果發現有值,這時會調用equals()方法來檢查兩個對象是否真的相同。如果兩者相同,HashSet就不會讓其加入操作成功。如果不同的話,就會重新散列到其他位置。這樣就大大減少了equals的次數,相應就大大提高了執行速度。
ArrayList和LinkedList區別
- ArrayList:基於動態數組,連續內存存儲,適合下標訪問(隨機訪問)
擴容機制:因為數組長度固定,超出長度存數據時按原長度的1.5倍新建數組,然后將老數組的數據拷貝到新數組(使用Arrays.copyOf()),如果不是尾部插入數據還會涉及到元素的移動(往后復制一份,插入新元素),使用尾插法並指定初始容量可以極大提升性能,甚至超過linkedList(因為它需要創建大量的node對象) - LinkedList:基於鏈表,可以存儲在分散的內存中,適合做數據插入及刪除操作,不適合查詢:需要逐一遍歷
遍歷LinkedList必須使用iterator不能使用for循環,因為每次for循環體內通過get(i)取得某一元素時都需要對list重新進行遍歷,性能消耗極大。 另外不要試圖使用indexOf等返回元素索引,並利用其進行遍歷,使用indexOf對list進行了遍歷,當結果為空時會遍歷整個列表。 - 補充) Vector類似於arraylist只是內部加synchronized鎖了,保證了線程安全,但效率低。
HashMap和HashTable有什么區別?其底層實現是什么?
(1)HashMap方法沒有synchronized修飾,線程非安全,HashTable線程安全; (2)HashMap允許key和value為null,而HashTable不允許
底層實現:數組+鏈表實現
jdk8開始鏈表高度到8,數組長度超過64,鏈表轉變為紅黑樹,元素以內部類Node節點存在
- 計算key的hash值,二次hash再跟數組長度-1做位運算,得到我們要存儲在數組的哪個下標下
- 如果沒有產生hash沖突(下標位置沒有元素),則直接創建Node存入數組
- 如果產生hash沖突,先進行equal比較,相同則取代該元素,不同,則判斷鏈表高度插入鏈表,鏈表高度達到8,並且數組長度到64則轉變為紅黑樹,長度低於6則將紅黑樹轉回鏈表
- 為null的key,存在下標0的位置
數組擴容同於ArrayList
ConcurrentHashMap原理,jdk7和jdk8版本的區別
執行put(k,v)的時候,先根據k進行一次hash,得到所在的segment段,然后二次hash得到它的段內的位置。每個段的鎖是獨立的,所以不同段之間不會存在線程阻塞,從而實現安全且效率。
談談LinkedHashMap和HashMap的區別
繼承於HashMap,所以先談談HashMap的底層。
1,初始化大小是16,如果事先知道數據量的大小,建議修改默認初始化大小。 減少擴容次數,提高性能 ,這是我一直會強調的點
2,最大的裝載因子默認是0.75,當HashMap中元素個數達到容量的0.75時,就會擴容。 容量是原先的兩倍
3,HashMap底層采用鏈表法來解決沖突。 但是存在一個問題,就是鏈表也可能會過長,影響性能 於是JDK1.8,對HashMap做了進一步的優化,引入了紅黑樹。 當鏈表長度超過8,且數組容量大於64時,鏈表就會轉換為紅黑樹當紅黑樹的節點數量小於6時,會將紅黑樹轉換為鏈表。 因為在數據量較小的情況下,紅黑樹要維護自身平衡,比鏈表性能沒有優勢。
其次,LinkedHashMap就是鏈表+散列表的結構,其底層采用了Linked雙向鏈表來保存節點的訪問順序,所以保證了有序性。
什么是字節碼?采用字節碼的好處是什么?
Java中引入了虛擬機的概念,即在機器和編譯程序之間加入了一層抽象的虛擬的機器。這台虛擬的機器在任何平台上都提供給編譯程序一個的共同的接口。編譯程序只需要面向虛擬機,生成虛擬機能夠理解的代碼,然后由解釋器來將虛擬機代碼轉換為特定系統的機器碼執行。在Java中,這種供虛擬機理解的代碼叫做字節碼(即擴展名為 .class的文件),它不面向任何特定的處理器,只面向虛擬機。每一種平台的解釋器是不同的,但是實現的虛擬機是相同的。Java源程序經過編譯器編譯后變成字節碼,字節碼由虛擬機解釋執行,虛擬機將每一條要執行的字節碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼,然后在特定的機器上運行。這也就是解釋了Java的編譯與解釋並存的特點。
Java源代碼---->編譯器---->jvm可執行的Java字節碼(即虛擬指令)---->jvm---->jvm中解釋器----->機器可執行的二進制機器碼---->程序運行
采用字節碼的好處:
Java語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以Java程序運行時比較高效,而且,由於字節碼並不專對一種特定的機器,因此,Java程序無須重新編譯便可在多種不同的計算機上運行。
IO流分類及選擇
- 分類
按方向分:輸入流,輸出流
按讀取的單位分:字節流,字符流
按處理的方式分:節點流,處理流
IO流的4大基類:InputStream,OutputStream,Reader,Writer - 選擇
字節流可以讀取任何文件
讀取文本文件的時候:選擇字符流(假如有解析文件的內容的需求,比如逐行處理,則采用字符流,比如txt文件)
讀取二進制文件的時候,選擇字節流(視頻,音頻,doc,ppt)
serialVersionUID的作用是什么
序列化是指把Java對象轉換為字節序列的過程,而反序列化是指把字節序列恢復為Java對象的過程。
當執行序列化時,我們寫對象到磁盤中,會根據當前這個類的結構生成一個版本號ID,當反序列化時,程序會比較磁盤中的序列化版本號ID跟當前的類結構生成的版本號ID是否一致,如果一致則反序列化成功,否則,反序列化失敗。加上版本號,有助於當我們的類結構發生了變化,依然可以之前已經序列化的對象反序列化成功。也就是新版本兼容老版本。
Java中的異常體系
Java中的所有異常都來自頂級父類Throwable。
Error是虛擬機內部錯誤
棧內存溢出錯誤:StackOverflowError(遞歸,遞歸層次太多或遞歸沒有結束)
堆內存溢出錯誤:OutOfMemoryError(堆創建了很多對象)
Exception是我們編寫的程序錯誤,分為RuntimeException和非運行時異常
RuntimeException:也稱為LogicException
為什么編譯器不會要求你去try catch處理?
本質是邏輯錯誤,比如空指針異常,這種問題是編程邏輯不嚴謹造成的
應該通過完善我們的代碼編程邏輯,來解決問題
例如:
算數異常,
空指針,
類型轉換異常,
數組越界,
NumberFormateException(數字格式異常,轉換失敗,比如“a12”就會轉換失敗)
非RuntimeException:
編譯器會要求我們try catch或者throws處理本質是客觀因素造成的問題,比如FileNotFoundException寫了一個程序,自動閱卷,需要讀取答案的路徑(用戶錄入),用戶可能錄入是一個錯誤的路徑,所以我們要提前預案,寫好發生異常之后的處理方式,這也是java程序健壯性的一種體現
例如:
IOException,
SQLException,
FileNotFoundException,
NoSuchFileException,
NoSuchMethodException
throw跟throws的區別
- throw,作用於方法內,用於主動拋出異常
- throws, 作用於方法聲明上,聲明該方法有可能會拋些某些異常
- 針對項目中,異常的處理方式,我們一般采用層層往上拋,最終通過異常處理機制統一處理(展示異常頁面,或返回統一的json信息),自定義異常一般繼承RunntimeException。
Java類加載器
JDK自帶有三個類加載器:
- bootstrap ClassLoader(啟動類加載器) 爺爺
- ExtClassLoader(擴展類加載器) 兒子
- AppClassLoader(應用類加載器) 孫子
BootStrapClassLoader是ExtClassLoader的父類加載器,默認負責加載%JAVA_HOME%lib下的jar包和class文件。ExtClassLoader是AppClassLoader的父類加載器,負責加載%JAVA_HOME%/lib/ext文件夾下的jar包和class類。AppClassLoader是自定義類加載器的父類,負責加載classpath下的類文件(自己寫的代碼以及引入的jar包)。是系統類加載器也是線程上下文加載器。
繼承ClassLoader實現自定義類加載器。
雙親委托模型
雙親委派模型的好處:
- 主要是為了安全性,避免用戶自己編寫的類動態替換 Java的一些核心類,比如 String。
- 同時也避免了類的重復加載,因為 JVM中區分不同類,不僅僅是根據類名,相同的 class文件被不 同的 ClassLoader加載就是不同的兩個類
GC如何判斷對象可以被回收
- 引用計數法:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收
- 可達性分析法(被java采用):從 GC Roots 開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的,那么虛擬機就判斷是可回收對象
引用計數法,可能會出現A 引用了 B,B 又引用了A,這時候就算他們都不再使用>了,但因為相互引用 計數器=1 永遠無法被回收。
GC Roots的對象有:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
可達性算法中的不可達對象並不是立即死亡的,對象擁有一次自我拯救的機會。對象被系統宣告死亡至少要經歷兩次標記過程:第一次是經過可達性分析發現沒有與GC Roots相連接的引用鏈,第二次是在由虛擬機自動建立的Finalizer隊列中判斷是否需要執行finalize()方法。
當對象變成(GC Roots)不可達時,GC會判斷該對象是否覆蓋了finalize方法,若未覆蓋,則直接將其回收。否則,若對象未執行過finalize方法,將其放入F-Queue隊列,由低優先級線程執行該隊列中對象的finalize方法。執行finalize方法完畢后,GC會再次判斷該對象是否可達,若不可達(finalize方法中沒有引入其他對象),則進行回收,否則,對象“復活”。
每個對象只能觸發一次finalize()方法。
線程並發相關
創建線程的方式
繼承Thread
實現Runable接口
實現Callable接口(可以獲取線程執行之后的返回值)
但實際后兩種,更准確的理解是創建了一個可執行的任務,要采用多線程的方式執行,還需要通過創建Thread對象來執行,比如 new Thread(new Runnable(){}).start();這樣的方式來執行。在實際開發中,我們通常采用線程池的方式來完成Thread的創建,更好管理線程資源。
- 如何正確啟動線程
class MyThread extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName()+":running....."); } } public static void main(String[] args){ MyThread thread = new MyThread(); //正確啟動線程的方式 //thread.run();//調用方法並非開啟新線程 thread.start(); }
- 案例:實現runnable只是創建了一個可執行任務,並不是一個線程
class MyTask implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+":running...."); } } public static void main(String[] args){ MyTask task = new MyTask(); //task.start(); //並不能直接以線程的方式來啟動 //它表達的是一個任務,需要啟動一個線程來執行 new Thread(task).start(); }
- 案例三:runnable vs callable
class MyTask2 implements Callable<Boolean>{ @Override public Boolean call() throws Exception { return null; } }
明確一點:
本質上來說創建線程的方式就是繼承Thread,就是線程池,內部也是創建好線程對象來執行任務。
線程的生命周期?線程有幾種狀態
- 線程通常有五種狀態,創建,就緒,運行、阻塞和死亡狀態。
(1) 新建狀態(New):新創建了一個線程對象
(2) 就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的start方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權
(3) 運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
(4) 阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。
(5) 死亡狀態(Dead):線程執行完了或者因異常退出了run方法,該線程結束生命周期。 - 阻塞的情況又分為三種:
(1) 等待阻塞:運行的線程執行wait方法,該線程會釋放占用的所有資源,JVM會把該線程放入“等待池”中。進入這個狀態后,是不能自動喚醒的,必須依靠其他線程調用notify或notifyAll方法才能被喚醒,wait是object類的方法
(2) 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入“鎖池”中。
(3) 其他阻塞:運行的線程執行sleep或join方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep狀態超時,join等待線程終止或者超時,或者I/O處理完畢時,線程重新轉入就緒狀態。 sleep是Thread類的方法
sleep()、wait()、join()、yield()的區別
- 鎖池
所有需要競爭同步鎖的線程都會放在鎖池當中,比如當前對象的鎖已經被其中一個線程得到,則其他線程需要在這個鎖池進行等待,當前面的線程釋放同步鎖后鎖池中的線程去競爭同步鎖,當某個線程得到后會進入就緒隊列進行等待cpu資源分配。 - 等待池
當我們調用wait()方法后,線程會放到等待池當中,等待池的線程是不會去競爭同步鎖。只有調用了notify()或notifyAll()后等待池的線程才會開始去競爭鎖,notify()是隨機從等待池選出一個線程放到鎖池,而notifyAll()是將等待池的所有線程放到鎖池當中。
- sleep 是Thread 類的靜態本地方法,wait 則是Object 類的本地方法
- sleep方法不會釋放lock,但是wait會釋放,而且會加入到等待隊列中
sleep就是把cpu的執行資格和執行權釋放出去,不再運行此線程,當定時時間結束再取回cpu資源,參與cpu的調度,獲取到cpu資源后就可以繼續運行了。而如果sleep時該線程有鎖,那么sleep不會釋放這個鎖,而是把鎖帶着進入了凍結狀態,也就是說其他需要這個鎖的線程根本不可能獲取到這個鎖。也就是說無法執行程 序。如果在睡眠期間其他線程調用了這個線程的interrupt方法,那么這個線程也會拋出 interruptexception異常返回,這點和wait是一樣的。
- sleep方法不依賴於同步器synchronized,但是wait需要依賴synchronized關鍵字
- sleep不需要被喚醒(休眠之后推出阻塞),但是wait需要(不指定時間需要被別人中斷)
- sleep 一般用於當前線程休眠,或者輪循暫停操作,wait 則多用於多線程之間的通信
- sleep 會讓出 CPU 執行時間且強制上下文切換,而 wait 則不一定,wait 后可能還是有機會重新競 爭到鎖繼續執行的。
yield()執行后線程直接進入就緒狀態,馬上釋放了cpu的執行權,但是依然保留了cpu的執行資格,所以有可能cpu下次進行線程調度還會讓這個線程獲取到執行權繼續執行。
join()執行后線程進入阻塞狀態,例如在線程B中調用線程A的join(),那線程B會進入到阻塞隊列,直到線程A結束或中斷線程
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("22222222"); } }); t1.start(); t1.join(); // 這行代碼必須要等t1全部執行完畢,才會執行 System.out.println("1111"); } 輸出結果 22222222 1111
對線程安全的理解
不是線程安全,應該是內存安全,堆是共享內存,可以被所有線程訪問
堆是進程和線程共有的空間,分全局堆和局部堆。全局堆就是所有沒有分配的空間,局部堆就是用戶分配的空間。堆在操作系統對進程初始化的時候分配,運行過程中也可以向系統要額外的堆,但是用完了要還給操作系統,要不然就是內存泄漏。
在Java中,堆是Java虛擬機所管理的內存中最大的一塊,是所有線程共享的一塊內存區域,在虛擬機啟動時創建。堆所存在的內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。
棧是每個線程獨有的,保存其運行狀態和局部自動變量的。棧在線程開始的時候初始化,每個線程的棧互相獨立,因此,棧是線程安全的。操作系統在切換線程的時候會自動切換棧。棧空間不需要在高級語言里面顯式的分配和釋放。
目前主流操作系統都是多任務的,即多個進程同時運行。為了保證安全,每個進程只能訪問分配給自己的內存空間,而不能訪問別的進程的,這是由操作系統保障的。在每個進程的內存空間中都會有一塊特殊的公共區域,通常稱為堆(內存)。進程內的所有線程都可以訪問到該區域,這就是造成問題的潛在原因。
對守護線程的理解
守護線程:為所有非守護線程(用戶線程)提供服務的線程。其他線程結束時,守護線程就會中斷。
注意: 由於守護線程的終止是自身無法控制的,因此千萬不要把IO、File等重要操作邏輯分配給它;因為它不靠譜;
守護線程的作用是什么?
舉例,GC垃圾回收線程:就是一個經典的守護線程,當我們的程序中不再有任何運行的Thread,程序就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態中運行,用於實時監控和管理系統中的可回收資源。
應用場景:
(1) 來為其它線程提供服務支持的情況;
(2) 或者在任何情況下,程序結束時,這個線程必須正常且立刻關閉,就可以作為守護線程來使用;反之,如果一個正在執行某個操作的線程必須要正確地關閉掉否則就會出現不好的后果的話,那么這個線程就不能是守護線程,而是用戶線程。通常都是些關鍵的事務,比方說,數據庫錄入或者更新,這些操作都是不能中斷的。
thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置為守護線程。
在Daemon線程中產生的新線程也是Daemon的。守護線程不能用於去訪問固有資源,比如讀寫操作或者計算邏輯。因為它會在任何時候甚至在一個操作
的中間發生中斷。
ThreadLocal的原理和使用場景
每一個 Thread 對象均含有一個 ThreadLocalMap 類型的成員變量 threadLocals ,它存儲本線程中所有ThreadLocal對象及其對應的值
ThreadLocalMap 由一個個Entry 對象構成
Entry 繼承自 WeakReference<ThreadLocal<?>> ,一個 Entry 由 ThreadLocal對象和 Object 構成。由此可見, Entry 的key是ThreadLocal對象,並且是一個弱引用。當沒指向key的強引用后,該 key就會被垃圾收集器回收
當執行set方法時,ThreadLocal首先會獲取當前線程對象,然后獲取當前線程的ThreadLocalMap對象。再以當前ThreadLocal對象為key,將值存儲進ThreadLocalMap對象中。
get方法執行過程類似。ThreadLocal首先會獲取當前線程對象,然后獲取當前線程的ThreadLocalMap 對象。再以當前ThreadLocal對象為key,獲取對應的value。
由於每一條線程均含有各自私有的ThreadLocalMap容器,這些容器相互獨立互不影響,因此不會存在線程安全性問題,從而也無需使用同步機制來保證多條線程訪問容器的互斥性。
使用場景:
- 在進行對象跨層傳遞的時候,使用ThreadLocal可以避免多次傳遞,打破層次間的約束
- 線程間數據隔離
- 進行事務操作,用於存儲線程事務信息
- 數據庫連接,Session會話管理。
Spring框架在事務開始時會給當前線程綁定一個Jdbc Connection,在整個事務過程都是使用該線程綁定的 connection來執行數據庫操作,實現了事務的隔離性。Spring框架里面就是用的ThreadLocal來實現這種隔離
ThreadLocal內存泄露原因,如何避免
內存泄露為程序在申請內存后,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積后果很嚴重,無論多少內存,遲早會被占光,不再會被使用的對象或者變量占用的內存不能被回收,就是內存泄露。
強引用:使用最普遍的引用(new),一個對象具有強引用,不會被垃圾回收器回收。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不回收這種對象。如果想取消強引用和某個對象之間的關聯,可以顯式地將引用賦值為null,這樣可以使JVM在合適的時間就會回收該對象。
弱引用:JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用 java.lang.ref.WeakReference類來表示。可以在緩存中使用弱引用。
ThreadLocal的實現原理,每一個Thread維護一個ThreadLocalMap,key為使用弱引用的ThreadLocal 實例,value為線程變量的副本
threadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal不存在外部強引用時, Key(ThreadLocal)勢必會被GC回收,這樣就會導致ThreadLocalMap中key為null, 而value還存在着強引用,只有thread線程退出以后,value的強引用鏈條才會斷掉,但如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈(紅色鏈條)
- key 使用強引用
當threadLocalMap的key為強引用回收ThreadLocal時,因為ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry內存泄漏。 - key 使用弱引用
當ThreadLocalMap的key為弱引用回收ThreadLocal時,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。當key為null,在下一次ThreadLocalMap調用 set(),get(),remove()方法的時候會被清除value值。
因此,ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。
ThreadLocal正確的使用方法
- 每次使用完ThreadLocal都調用它的remove()方法清除數據
- 將ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強引用,也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值,進而清除掉 。
並發、並行、串行的區別
- 串行在時間上不可能發生重疊,前一個任務沒搞定,下一個任務就只能等着
- 並行在時間上是重疊的,兩個任務在同一時刻互不干擾的同時執行。
- 並發允許兩個任務彼此干擾。統一時間點,只有一個任務運行,交替執行
並發的三大特性
- 原子性
原子性是指在一個操作中cpu不可以在中途暫停然后再調度,即不被中斷操作,要不全部執行完成,要不都不執行。就好比轉賬,從賬戶A向賬戶B轉1000元,那么必然包括2個操作:從賬戶A減去1000元, 往賬戶B加上1000元。2個操作必須全部完成。
private long count = 0; public void calc() { count++; }
- 1:將 count 從主存讀到工作內存中的副本中
- 2:+1的運算
- 3:將結果寫入工作內存
- 4:將工作內存的值刷回主存(什么時候刷入由操作系統決定,不確定的)
那程序中原子性指的是最小的操作單元,比如自增操作,它本身其實並不是原子性操作,分了3步的, 包括讀取變量的原始值、進行加1操作、寫入工作內存。所以在多線程中,有可能一個線程還沒自增完,可能才執行到第二部,另一個線程就已經讀取了值,導致結果錯誤。那如果我們能保證自增操作是一個原子性的操作,那么就能保證其他線程讀取到的一定是自增后的數據。
關鍵字:synchronized
- 可見性
當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。若兩個線程在不同的cpu,那么線程1改變了i的值還沒刷新到主存,線程2又使用了i,那么這個i值肯定還是之前的,線程1對變量的修改線程沒看到這就是可見性問題。
//線程1 boolean stop = false; while(!stop){ doSomething(); } //線程2 stop = true;
如果線程2改變了stop的值,線程1一定會停止嗎?不一定。當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉去做其他事情了,那么線程1由於不知道線程2對stop變量的更改,因此還會一直循環下去。
關鍵字:volatile、synchronized、final
- 有序性
虛擬機在進行代碼編譯時,對於那些改變順序之后不會對最終結果造成影響的代碼,虛擬機不一定會按照我們寫的代碼的順序來執行,有可能將他們重排序。實際上,對於有些代碼進行重排序之后,雖然對變量的值沒有造成影響,但有可能會出現線程安全問題。
int a = 0; bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } }
write方法里的1和2做了重排序,線程1先對flag賦值為true,隨后執行到線程2,ret直接計算出結果, 再到線程1,這時候a才賦值為2,很明顯遲了一步。
關鍵字:volatile、synchronized
volatile本身就包含了禁止指令重排序的語義,而synchronized關鍵字是由“一個變量在同一時刻只允許一條線程對其進行lock操作”這條規則明確的。
synchronized關鍵字同時滿足以上三種特性,但是volatile關鍵字不滿足原子性。 在某些情況下,volatile的同步機制的性能確實要優於鎖(使用synchronized關鍵字或
java.util.concurrent包里面的鎖),因為volatile的總開銷要比鎖低。 我們判斷使用volatile還是加鎖的唯一依據就是volatile的語義能否滿足使用的場景(原子性)
volatile
- 保證被volatile修飾的共享變量對所有線程總是可見的,也就是當一個線程修改了一個被volatile修飾共享變量的值,新值總是可以被其他線程立即得知。
//線程1 boolean stop = false; while(!stop){ doSomething(); } //線程2 stop = true;
如果線程2改變了stop的值,線程1一定會停止嗎?不一定。當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉去做其他事情了,那么線程1由於不知道線程2對stop變量的更改,因此還會一直循環下去。
- 禁止指令重排序優化。
int a = 0; bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } }
write方法里的1和2做了重排序,線程1先對flag賦值為true,隨后執行到線程2,ret直接計算出結果,再到線程1,這時候a才賦值為2,很明顯遲了一步。
但是用volatile修飾之后
(1) 使用volatile關鍵字會強制將修改的值立即寫入主存
(2) 使用volatile關鍵字的話,當線程2進行修改時,會導致線程1的工作內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效)
(3) 由於線程1的工作內存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。
i++:其實是兩個步驟,先加加,然后再賦值。不是原子性操作,所以volatile不能保證線程安全
為什么用線程池?解釋下線程池參數?
- 降低資源消耗;提高線程利用率,降低創建和銷毀線程的消耗。
- 提高響應速度;任務來了,直接有線程可用可執行,而不是先創建線程,再執行。
- 提高線程的可管理性;線程是稀缺資源,使用線程池可以統一分配調優監控。
- corePoolSize代表核心線程數,也就是正常情況下創建工作的線程數,這些線程創建后並不會消除,而是一種常駐線程
- maxinumPoolSize 代表的是最大線程數,它與核心線程數相對應,表示最大允許被創建的線程數,比如當前任務較多,將核心線程數都用完了,還無法滿足需求時,此時就會創建新的線程,但是線程池內線程總數不會超過最大線程數
- keepAliveTime、unit 表示超出核心線程數之外的線程的空閑存活時間,也就是核心線程不會消除,但是超出核心線程數的部分線程如果空閑一定的時間則會被消除,我們可以通過setKeepAliveTime 來設置空閑時間
- workQueue 用來存放待執行的任務,假設我們現在核心線程都已被使用,還有任務進來則全部放入隊列,直到整個隊列被放滿但任務還再持續進入則會開始創建新的線程
- ThreadFactory實際上是一個線程工廠,用來生產線程執行任務。我們可以選擇使用默認的創建工廠,產生的線程都在同一個組內,擁有相同的優先級,且都不是守護線程。當然我們也可以選擇自定義線程工廠,一般我們會根據業務來制定不同的線程工廠
- Handler任務拒絕策略,有兩種情況,第一種是當我們調用shutdown等方法關閉線程池后,這時候即使線程池內部還有沒執行完的任務正在執行,但是由於線程池已經關閉,我們再繼續想線程池提交任務就會遭到拒絕。另一種情況就是當達到最大線程數,線程池已經沒有能力繼續處理新提交的任務時,這是也就拒絕
簡述線程池處理流程
線程池中阻塞隊列的作用?為什么是先添加列隊而不是先創建最大線程?
-
一般的隊列只能保證作為一個有限長度的緩沖區,如果超出了緩沖長度,就無法保留當前的任務了,阻塞隊列通過阻塞可以保留住當前想要繼續入隊的任務。阻塞隊列可以保證任務隊列中沒有任務時阻塞獲取任務的線程,使得線程進入wait狀態,釋放cpu資源。
阻塞隊列自帶阻塞和喚醒的功能,不需要額外處理,無任務執行時,線程池利用阻塞隊列的take方法掛起,從而維持核心線程的存活、不至於一直占用cpu資源 -
在創建新線程的時候,是要獲取全局鎖的,這個時候其它的就得阻塞,影響了整體效率。就好比一個企業里面有10個(core)正式工的名額,最多招10個正式工,要是任務超過正式工人數(task > core)的情況下,工廠領導(線程池)不是首先擴招工人,還是這10人,但是任務可以稍微積壓一下,即先放到隊列去(代價低)。10個正式工慢慢干,遲早會干完的,要是任務還在繼續增加,超過正式工的加班忍耐極限了(隊列滿了),就的招外包幫忙了(注意是臨時工)要是正式工加上外包還是不能完成任務,那新來的任務就會被領導拒絕了(線程池的拒絕策略)。
線程池中線程復用原理
線程池將線程和任務進行解耦,線程是線程,任務是任務,擺脫了之前通過Thread創建線程時的一個線程必須對應一個任務的限制。
在線程池中,同一個線程可以從阻塞隊列中不斷獲取新任務來執行,其核心原理在於線程池對Thread進行了封裝,並不是每次執行任務都會調用Thread.start()來創建新線程,而是讓每個線程去執行一個“循環任務”,在這個“循環任務”中不停檢查是否有任務需要被執行,如果有則直接執行,也就是調用任務中run方法,將run方法當成一個普通的方法執行,通過這種方式只使用固定的線程就將所有任務的run方法串聯起來。
