Java知識點全面彙總
相關概念
面向物件的三個特徵
封裝,繼承,多型,這個應該是人人皆知,有時候也會加上抽象。
多型的好處
允許不同類物件對同一訊息做出響應,即同一訊息可以根據傳送物件的不同而採用多種不同的行為方式(傳送訊息就是函式呼叫)。主要有以下優點:
-
可替換性:多型對已存在程式碼具有可替換性
-
可擴充性:增加新的子類不影響已經存在的類結構
-
介面性:多型是超類通過方法簽名,向子類提供一個公共介面,由子類來完善或者重寫它來實現的。
-
靈活性
-
簡化性
程式碼中如何實現多型
實現多型主要有以下三種方式:
1. 介面實現
2. 繼承父類重寫方法
3. 同一類中進行方法過載
虛擬機器是如何實現多型的
動態繫結技術(dynamic binding),執行期間判斷所引用物件的實際型別,根據實際型別呼叫對應的方法。
介面的意義
介面的意義用三個詞就可以概括:規範,擴充套件,回撥。
抽象類的意義
抽象類的意義可以用三句話來概括:
-
為其他子類提供一個公共的型別
-
封裝子類中重複定義的內容
-
定義抽象方法,子類雖然有不同的實現,但是定義時一致的
父類的靜態方法能否被子類重寫
不能。重寫只適用於例項方法,不能用於靜態方法,而子類當中含有和父類相同簽名的靜態方法,我們一般稱之為隱藏。
什麼是不可變物件
不可變物件指物件一旦被建立,狀態就不能再改變。任何修改都會建立一個新的物件,如 String、Integer及其它包裝類
靜態變數和例項變數的區別?(使用範圍不一樣)
靜態變數儲存在方法區,屬於類所有。例項變數儲存在堆當中,其引用存在當前執行緒棧。
能否建立一個包含可變物件的不可變物件?
當然可以建立一個包含可變物件的不可變物件的,你只需要謹慎一點,不要共享可變物件的引用就可以了,如果需要變化時,就返回原物件的一個拷貝。最常見的例子就是物件中包含一個日期物件的引用。
java 建立物件的幾種方式
-
採用new
-
通過反射
-
採用clone
-
通過序列化機制
前2者都需要顯式地呼叫構造方法。造成耦合性最高的恰好是第一種,因此你發現無論什麼框架,只要涉及到解耦必先減少new的使用。
switch中能否使用string做引數
在idk 1.7之前,switch只能支援byte, short, char, int或者其對應的封裝類以及Enum型別。從idk 1.7之後switch開始支援String。
switch能否作用在byte, long上?
可以用在byte上,但是不能用在long上。
String s1=”ab”, String s2=”a”+”b”, String s3=”a”, String s4=”b”, s5=s3+s4請問s5==s2返回什麼?
返回false。在編譯過程中,編譯器會將s2直接優化為”ab”,會將其放置在常量池當中,s5則是被建立在堆區,相當於s5=new String(“ab”);
你對String物件的intern()熟悉麼?
intern()方法會首先從常量池中查詢是否存在該常量值,如果常量池中不存在則現在常量池中建立,如果已經存在則直接返回。
比如
String s1=”aa”;
String s2=s1.intern();
System.out.print(s1==s2);//返回true
Object中有哪些公共方法?
-
equals()
-
clone()
-
getClass()
-
notify(),notifyAll(),wait()
-
toString
java當中的四種引用
強引用,軟引用,弱引用,虛引用。不同的引用型別主要體現在GC上:
-
強引用:如果一個物件具有強引用,它就不會被垃圾回收器回收。即使當前記憶體空間不足,JVM也不會回收它,而是丟擲 OutOfMemoryError 錯誤,使程式異常終止。如果想中斷強引用和某個物件之間的關聯,可以顯式地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該物件。
-
軟引用:在使用軟引用時,如果記憶體的空間足夠,軟引用就能繼續被使用,而不會被垃圾回收器回收,只有在記憶體不足時,軟引用才會被垃圾回收器回收。
-
弱引用:具有弱引用的物件擁有的生命週期更短暫。因為當 JVM 進行垃圾回收,一旦發現弱引用物件,無論當前記憶體空間是否充足,都會將弱引用回收。不過由於垃圾回收器是一個優先順序較低的執行緒,所以並不一定能迅速發現弱引用物件。
-
虛引用:顧名思義,就是形同虛設,如果一個物件僅持有虛引用,那麼它相當於沒有引用,在任何時候都可能被垃圾回收器回收。
更多瞭解參見深入物件引用:
http://blog.csdn.net/dd864140130/article/details/49885811
WeakReference與SoftReference的區別?
這點在四種引用型別中已經做了解釋,這裡簡單說明一下即可:
雖然 WeakReference 與 SoftReference 都有利於提高 GC 和 記憶體的效率,但是 WeakReference ,一旦失去最後一個強引用,就會被 GC 回收,而軟引用雖然不能阻止被回收,但是可以延遲到 JVM 記憶體不足的時候。
為什麼要有不同的引用型別
不像C語言,我們可以控制記憶體的申請和釋放,在Java中有時候我們需要適當的控制物件被回收的時機,因此就誕生了不同的引用型別,可以說不同的引用型別實則是對GC回收時機不可控的妥協。有以下幾個使用場景可以充分的說明:
-
利用軟引用和弱引用解決OOM問題:用一個HashMap來儲存圖片的路徑和相應圖片物件關聯的軟引用之間的對映關係,在記憶體不足時,JVM會自動回收這些快取圖片物件所佔用的空間,從而有效地避免了OOM的問題.
-
通過軟引用實現Java物件的快取記憶體:比如我們建立了一Person的類,如果每次需要查詢一個人的資訊,哪怕是幾秒中之前剛剛查詢過的,都要重新構建一個例項,這將引起大量Person物件的消耗,並且由於這些物件的生命週期相對較短,會引起多次GC影響效能。此時,通過軟引用和 HashMap 的結合可以構建快取記憶體,提供效能。
java中==和eqauls()
的區別,equals()
和`hashcode的區別
==是運算子,用於比較兩個變數是否相等,而equals是Object類的方法,用於比較兩個物件是否相等。預設Object類的equals方法是比較兩個物件的地址,此時和==的結果一樣。換句話說:基本型別比較用==,比較的是他們的值。預設下,物件用==比較時,比較的是記憶體地址,如果需要比較物件內容,需要重寫equal方法。
equals()
和hashcode()
的聯絡
hashCode()
是Object類的一個方法,返回一個雜湊值。如果兩個物件根據equal()方法比較相等,那麼呼叫這兩個物件中任意一個物件的hashCode()方法必須產生相同的雜湊值。
如果兩個物件根據eqaul()方法比較不相等,那麼產生的雜湊值不一定相等(碰撞的情況下還是會相等的。)
a.hashCode()有什麼用?與a.equals(b)有什麼關係
hashCode() 方法是相應物件整型的 hash 值。它常用於基於 hash 的集合類,如 Hashtable、HashMap、LinkedHashMap等等。它與 equals() 方法關係特別緊密。根據 Java 規範,使用 equal() 方法來判斷兩個相等的物件,必須具有相同的 hashcode。
將物件放入到集合中時,首先判斷要放入物件的hashcode是否已經在集合中存在,不存在則直接放入集合。如果hashcode相等,然後通過equal()方法判斷要放入物件與集合中的任意物件是否相等:如果equal()判斷不相等,直接將該元素放入集合中,否則不放入。
有沒有可能兩個不相等的物件有相同的hashcode
有可能,兩個不相等的物件可能會有相同的 hashcode 值,這就是為什麼在 hashmap 中會有衝突。如果兩個物件相等,必須有相同的hashcode 值,反之不成立。
可以在hashcode中使用隨機數字嗎?
不行,因為同一物件的 hashcode 值必須是相同的
a==b與a.equals(b)有什麼區別
如果a 和b 都是物件,則 a==b 是比較兩個物件的引用,只有當 a 和 b 指向的是堆中的同一個物件才會返回 true,而 a.equals(b) 是進行邏輯比較,所以通常需要重寫該方法來提供邏輯一致性的比較。例如,String 類重寫 equals() 方法,所以可以用於兩個不同物件,但是包含的字母相同的比較。
3*0.1==0.3
返回值是什麼
false,因為有些浮點數不能完全精確的表示出來。
a=a+b與a+=b有什麼區別嗎?
+=操作符會進行隱式自動型別轉換,此處a+=b隱式的將加操作的結果型別強制轉換為持有結果的型別,而a=a+b則不會自動進行型別轉換。如:
byte a = 127;
byte b = 127;
b = a + b; // error : cannot convert from int to byte
b += a; // ok
(譯者注:這個地方應該表述的有誤,其實無論 a+b 的值為多少,編譯器都會報錯,因為 a+b 操作會將 a、b 提升為 int 型別,所以將 int 型別賦值給 byte 就會編譯出錯)
short s1= 1; s1 = s1 + 1; 該段程式碼是否有錯,有的話怎麼改?
有錯誤,short型別在進行運算時會自動提升為int型別,也就是說s1+1
的運算結果是int型別。
short s1= 1; s1 += 1; 該段程式碼是否有錯,有的話怎麼改?
+=操作符會自動對右邊的表示式結果強轉匹配左邊的資料型別,所以沒錯。
& 和 &&的區別
首先記住&是位操作,而&&是邏輯運算子。另外需要記住邏輯運算子具有短路特性,而&不具備短路特性。
public class Test{
static String name;
public static void main(String[] args){
if(name!=null&userName.equals("")){
System.out.println("ok");
}else{
System.out.println("erro");
}
}
}
以上程式碼將會丟擲空指標異常。
一個java檔案內部可以有類?(非內部類)
只能有一個public公共類,但是可以有多個default修飾的類。
如何正確的退出多層巢狀迴圈?
使用標號和break;
通過在外層迴圈中新增識別符號
內部類的作用
內部類可以有多個例項,每個例項都有自己的狀態資訊,並且與其他外圍物件的資訊相互獨立.在單個外圍類當中,可以讓多個內部類以不同的方式實現同一介面,或者繼承同一個類.建立內部類物件的時刻不依賴於外部類物件的建立。內部類並沒有令人疑惑的”is-a”管系,它就像是一個獨立的實體。
內部類提供了更好的封裝,除了該外圍類,其他類都不能訪問。
final, finalize和finally的不同之處
final 是一個修飾符,可以修飾變數、方法和類。如果 final 修飾變數,意味著該變數的值在初始化後不能被改變。finalize 方法是在物件被回收之前呼叫的方法,給物件自己最後一個復活的機會,但是什麼時候呼叫 finalize 沒有保證。finally 是一個關鍵字,與 try 和 catch 一起用於異常的處理。finally 塊一定會被執行,無論在 try 塊中是否有發生異常。
clone()是哪個類的方法?
java.lang.Cloneable 是一個標示性介面,不包含任何方法,clone 方法在 object 類中定義。並且需要知道 clone() 方法是一個本地方法,這意味著它是由 c 或 c++ 或 其他本地語言實現的。
深拷貝和淺拷貝的區別是什麼?
淺拷貝:被複制物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向原來的物件。換言之,淺拷貝僅僅複製所考慮的物件,而不復制它所引用的物件。
深拷貝:被複制物件的所有變數都含有與原來的物件相同的值,而那些引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深拷貝把要複製的物件所引用的物件都複製了一遍。
static都有哪些用法?
幾乎所有的人都知道static關鍵字這兩個基本的用法:靜態變數和靜態方法。也就是被static所修飾的變數/方法都屬於類的靜態資源,類例項所共享。
除了靜態變數和靜態方法之外,static也用於靜態塊,多用於初始化操作:
public calss PreCache{
static{
//執行相關操作
}
}
此外static也多用於修飾內部類,此時稱之為靜態內部類。
最後一種用法就是靜態導包,即import static
.import static是在JDK 1.5之後引入的新特性,可以用來指定匯入某個類中的靜態資源,並且不需要使用類名。資源名,可以直接使用資源名,比如:
import static java.lang.Math.*;
public class Test{
public static void main(String[] args){
//System.out.println(Math.sin(20));傳統做法
System.out.println(sin(20));
}
}
final有哪些用法
final也是很多面試喜歡問的地方,能回答下以下三點就不錯了:
1.被final修飾的類不可以被繼承
2.被final修飾的方法不可以被重寫
3.被final修飾的變數不可以被改變。如果修飾引用,那麼表示引用不可變,引用指向的內容可變。
4.被final修飾的方法,JVM會嘗試將其內聯,以提高執行效率
5.被final修飾的常量,在編譯階段會存入常量池中。
回答出編譯器對final域要遵守的兩個重排序規則更好:
1.在建構函式內對一個final域的寫入,與隨後把這個被構造物件的引用賦值給一個引用變數,這兩個操作之間不能重排序。
2.初次讀一個包含final域的物件的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。
資料型別相關
java中int char,long各佔多少位元組?
型別 | 位數 | 位元組數 |
---|---|---|
short | 2 | 16 |
int | 4 | 32 |
long | 8 |
64 |
char | 2 | 16 |
float | 4 | 32 |
double | 8 | 64 |
64位的JVM當中,int的長度是多少?
Java 中,int 型別變數的長度是一個固定值,與平臺無關,都是 32 位。意思就是說,在 32 位 和 64 位 的Java 虛擬機器中,int 型別的長度是相同的。
int和Integer的區別
Integer是int的包裝型別,在拆箱和裝箱中,二者自動轉換。int是基本型別,直接存數值,而integer是物件,用一個引用指向這個物件。
int 和Integer誰佔用的記憶體更多?
Integer 物件會佔用更多的記憶體。Integer是一個物件,需要儲存物件的元資料。但是 int 是一個原始型別的資料,所以佔用的空間更少。
String, StringBuffer和StringBuilder區別
String是字串常量,final修飾:StringBuffer字串變數(執行緒安全);
StringBuilder 字串變數(執行緒不安全)。
String和StringBuffer
String和StringBuffer主要區別是效能:String是不可變物件,每次對String型別進行操作都等同於產生了一個新的String物件,然後指向新的String物件。所以儘量不在對String進行大量的拼接操作,否則會產生很多臨時物件,導致GC開始工作,影響系統性能。
StringBuffer是對物件本身操作,而不是產生新的物件,因此在有大量拼接的情況下,我們建議使用StringBuffer。
但是需要注意現在JVM會對String拼接做一定的優化:String s=“This is only ”+”simple”+”test”
會被虛擬機器直接優化成String s=“This is only simple test”
,此時就不存在拼接過程。
StringBuffer和StringBuilder
StringBuffer是執行緒安全的可變字串,其內部實現是可變陣列。StringBuilder是jdk 1.5新增的,其功能和StringBuffer類似,但是非執行緒安全。因此,在沒有多執行緒問題的前提下,使用StringBuilder會取得更好的效能。
什麼是編譯器常量?使用它有什麼風險?
公共靜態不可變(public static final )變數也就是我們所說的編譯期常量,這裡的 public 可選的。實際上這些變數在編譯時會被替換掉,因為編譯器知道這些變數的值,並且知道這些變數在執行時不能改變。這種方式存在的一個問題是你使用了一個內部的或第三方庫中的公有編譯時常量,但是這個值後面被其他人改變了,但是你的客戶端仍然在使用老的值,甚至你已經部署了一個新的jar。為了避免這種情況,當你在更新依賴 JAR 檔案時,確保重新編譯你的程式。
java當中使用什麼型別表示價格比較好?
如果不是特別關心記憶體和效能的話,使用BigDecimal,否則使用預定義精度的 double 型別。
如何將byte轉為String
可以使用 String 接收 byte[] 引數的構造器來進行轉換,需要注意的點是要使用的正確的編碼,否則會使用平臺預設編碼,這個編碼可能跟原來的編碼相同,也可能不同。
可以將int強轉為byte型別麼?會產生什麼問題?
我們可以做強制轉換,但是Java中int是32位的而byte是8 位的,所以,如果強制轉化int型別的高24位將會被丟棄,byte 型別的範圍是從-128到128
關於垃圾回收
你知道哪些垃圾回收演算法?
垃圾回收從理論上非常容易理解,具體的方法有以下幾種:
1. 標記-清除
2. 標記-複製
3. 標記-整理
4. 分代回收
更詳細的內容參見深入理解垃圾回收演算法:點選開啟連結
如何判斷一個物件是否應該被回收
這就是所謂的物件存活性判斷,常用的方法有兩種:1.引用計數法; 2.物件可達性分析。由於引用計數法存在互相引用導致無法進行GC的問題,所以目前JVM虛擬機器多使用物件可達性分析演算法。
簡單的解釋一下垃圾回收
Java 垃圾回收機制最基本的做法是分代回收。記憶體中的區域被劃分成不同的世代,物件根據其存活的時間被儲存在對應世代的區域中。一般的實現是劃分成3個世代:年輕、年老和永久(新生,老年,持久)。記憶體的分配是發生在年輕世代中的。當一個物件存活時間足夠長的時候,它就會被複製到年老世代中。對於不同的世代可以使用不同的垃圾回收演算法。進行世代劃分的出發點是對應用中物件存活時間進行研究之後得出的統計規律。一般來說,一個應用中的大部分物件的存活時間都很短。比如區域性變數的存活時間就只在方法的執行過程中。基於這一點,對於年輕世代的垃圾回收演算法就可以很有針對性。
呼叫System.gc()會發生什麼?
通知GC開始工作,但是GC真正開始的時間不確定。
程序,執行緒相關
說說程序,執行緒,協程之間的區別
簡而言之,程序是程式執行和資源分配的基本單位,一個程式至少有一個程序,一個程序至少有一個執行緒。程序在執行過程中擁有獨立的記憶體單元,而多個執行緒共享記憶體資源,減少切換次數,從而效率更高。執行緒是程序的一個實體,是cpu排程和分派的基本單位,是比程式更小的能獨立執行的基本單位。同一程序中的多個執行緒之間可以併發執行。
你瞭解守護執行緒嗎?它和非守護執行緒有什麼區別
程式執行完畢,jvm會等待非守護執行緒完成後關閉,但是jvm不會等待守護執行緒。守護執行緒最典型的例子就是GC執行緒。
什麼是多執行緒上下文切換
多執行緒的上下文切換是指CPU控制權由一個已經正在執行的執行緒切換到另外一個就緒並等待獲取CPU執行權的執行緒的過程。
建立兩種執行緒的方式?他們有什麼區別?
通過實現java.lang.Runnable或者通過擴充套件java.lang.Thread類。相比擴充套件Thread,實現Runnable介面可能更優.原因有二:
-
Java不支援多繼承。因此擴充套件Thread類就代表這個子類不能擴充套件其他類。而實現Runnable介面的類還可能擴充套件另一個類。
-
類可能只要求可執行即可,因此繼承整個Thread類的開銷過大。
Thread類中的start()和run()方法有什麼區別?
start()方法被用來啟動新建立的執行緒,而且start()內部呼叫了run()方法,這和直接呼叫run()方法的效果不一樣。當你呼叫run()方法的時候,只會是在原來的執行緒中呼叫,沒有新的執行緒啟動,start()方法才會啟動新執行緒。
怎麼檢測一個執行緒是否持有物件監視器
Thread類提供了一個holdsLock(Object obj)方法,當且僅當物件obj的監視器被某條執行緒持有的時候才會返回true,注意這是一個static方法,這意味著”某條執行緒”指的是當前執行緒。
Runnable和Callable的區別
Runnable介面中的run()方法的返回值是void,它做的事情只是純粹地去執行run()方法中的程式碼而已;Callable介面中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合可以用來獲取非同步執行的結果。
這其實是很有用的一個特性,因為多執行緒相比單執行緒更難、更復雜的一個重要原因就是因為多執行緒充滿著未知性,某條執行緒是否執行了?某條執行緒執行了多久?某條執行緒執行的時候我們期望的資料是否已經賦值完畢?無法得知,我們能做的只是等待這條多執行緒的任務執行完畢而已。而Callable+Future/FutureTask卻可以方便獲取多執行緒執行的結果,可以在等待時間太長沒獲取到需要的資料的情況下取消該執行緒的任務。
什麼導致執行緒阻塞
阻塞指的是暫停一個執行緒的執行以等待某個條件發生(如某資源就緒),學過作業系統的同學對它一定已經很熟悉了。Java 提供了大量方法來支援阻塞,下面讓我們逐一分析。
方法 | 說明 |
---|---|
sleep() | sleep() 允許 指定以毫秒為單位的一段時間作為引數,它使得執行緒在指定的時間內進入阻塞狀態,不能得到CPU 時間,指定的時間一過,執行緒重新進入可執行狀態。 典型地,sleep() 被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓執行緒阻塞一段時間後重新測試,直到條件滿足為止 |
suspend() 和 resume() | 兩個方法配套使用,suspend()使得執行緒進入阻塞狀態,並且不會自動恢復,必須其對應的resume() 被呼叫,才能使得執行緒重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個執行緒產生的結果的情形:測試發現結果還沒有產生後,讓執行緒阻塞,另一個執行緒產生了結果後,呼叫 resume() 使其恢復。 |
yield() | yield() 使當前執行緒放棄當前已經分得的CPU 時間,但不使當前執行緒阻塞,即執行緒仍處於可執行狀態,隨時可能再次分得 CPU 時間。呼叫 yield() 的效果等價於排程程式認為該執行緒已執行了足夠的時間從而轉到另一個執行緒 |
wait() 和 notify() | 兩個方法配套使用,wait() 使得執行緒進入阻塞狀態,它有兩種形式,一種允許 指定以毫秒為單位的一段時間作為引數,另一種沒有引數,前者當對應的 notify() 被呼叫或者超出指定時間時執行緒重新進入可執行狀態,後者則必須對應的 notify() 被呼叫。 |
wait(),notify()和suspend(),resume()之間的區別
初看起來它們與 suspend() 和 resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的所有方法,阻塞時都不會釋放佔用的鎖(如果佔用了的話),而這一對方法則相反。上述的核心區別導致了一系列的細節上的區別。
首先,前面敘述的所有方法都隸屬於 Thread 類,但是這一對卻直接隸屬於 Object 類,也就是說,所有物件都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因為這一對方法阻塞時要釋放佔用的鎖,而鎖是任何物件都具有的,呼叫任意物件的 wait() 方法導致執行緒阻塞,並且該物件上的鎖被釋放。而呼叫 任意物件的notify()方法則導致從呼叫該物件的 wait() 方法而阻塞的執行緒中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。
其次,前面敘述的所有方法都可在任何位置呼叫,但是這一對方法卻必須在 synchronized 方法或塊中呼叫,理由也很簡單,只有在synchronized 方法或塊中當前執行緒才佔有鎖,才有鎖可以釋放。同樣的道理,呼叫這一對方法的物件上的鎖必須為當前執行緒所擁有,這樣才有鎖可以釋放。因此,這一對方法呼叫必須放置在這樣的 synchronized 方法或塊中,該方法或塊的上鎖物件就是呼叫這一對方法的物件。若不滿足這一條件,則程式雖然仍能編譯,但在執行時會出現IllegalMonitorStateException 異常。
wait() 和 notify() 方法的上述特性決定了它們經常和synchronized關鍵字一起使用,將它們和作業系統程序間通訊機制作一個比較就會發現它們的相似性:synchronized方法或塊提供了類似於作業系統原語的功能,它們的執行不會受到多執行緒機制的干擾,而這一對方法則相當於 block 和wakeup 原語(這一對方法均宣告為 synchronized)。它們的結合使得我們可以實現作業系統上一系列精妙的程序間通訊的演算法(如訊號量演算法),並用於解決各種複雜的執行緒間通訊問題。
關於 wait() 和 notify() 方法最後再說明兩點:
第一:呼叫 notify() 方法導致解除阻塞的執行緒是從因呼叫該物件的 wait() 方法而阻塞的執行緒中隨機選取的,我們無法預料哪一個執行緒將會被選擇,所以程式設計時要特別小心,避免因這種不確定性而產生問題。
第二:除了 notify(),還有一個方法 notifyAll() 也可起到類似作用,唯一的區別在於,呼叫 notifyAll() 方法將把因呼叫該物件的 wait() 方法而阻塞的所有執行緒一次性全部解除阻塞。當然,只有獲得鎖的那一個執行緒才能進入可執行狀態。
談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend() 方法和不指定超時期限的 wait() 方法的呼叫都可能產生死鎖。遺憾的是,Java 並不在語言級別上支援死鎖的避免,我們在程式設計中必須小心地避免死鎖。
以上我們對 Java 中實現執行緒阻塞的各種方法作了一番分析,我們重點分析了 wait() 和 notify() 方法,因為它們的功能最強大,使用也最靈活,但是這也導致了它們的效率較低,較容易出錯。實際使用中我們應該靈活使用各種方法,以便更好地達到我們的目的。
產生死鎖的條件(互請不迴圈)
1.互斥條件:一個資源每次只能被一個程序使用。
2.請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
3.不剝奪條件:程序已獲得的資源,在未使用完之前,不能強行剝奪。
4.迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
為什麼wait()方法和notify()/notifyAll()方法要在同步塊中被呼叫
這是JDK強制的,wait()方法和notify()/notifyAll()方法在呼叫前都必須先獲得物件的鎖
wait()方法和notify()/notifyAll()方法在放棄物件監視器時有什麼區別
wait()方法和notify()/notifyAll()方法在放棄物件監視器的時候的區別在於:wait()方法立即釋放物件監視器,notify()/notifyAll()方法則會等待執行緒剩餘程式碼執行完畢才會放棄物件監視器。
wait()與sleep()的區別
關於這兩者已經在上面進行詳細的說明,這裡就做個概括好了:
-
sleep()來自Thread類,和wait()來自Object類。呼叫sleep()方法的過程中,執行緒不會釋放物件鎖。而 呼叫 wait 方法執行緒會釋放物件鎖
-
sleep()睡眠後不出讓系統資源,wait讓其他執行緒可以佔用CPU
-
sleep(milliseconds)需要指定一個睡眠時間,時間一到會自動喚醒.而wait()可需要配合notify()或者notifyAll()使用
為什麼wait, nofity和nofityAll這些方法不放在Thread類當中
一個很明顯的原因是JAVA提供的鎖是物件級的而不是執行緒級的,每個物件都有鎖,通過執行緒獲得。如果執行緒需要等待某些鎖那麼呼叫物件中的wait()方法就有意義了。如果wait()方法定義在Thread類中,執行緒正在等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因為鎖屬於物件。
怎麼喚醒一個阻塞的執行緒
如果執行緒是因為呼叫了wait()、sleep()或者join()方法而導致的阻塞,可以中斷執行緒,並且通過丟擲InterruptedException來喚醒它;如果執行緒遇到了IO阻塞,無能為力,因為IO是作業系統實現的,Java程式碼並沒有辦法直接接觸到作業系統。
什麼是多執行緒的上下文切換
多執行緒的上下文切換是指CPU控制權由一個已經正在執行的執行緒切換到另外一個就緒並等待獲取CPU執行權的執行緒的過程。
synchronized和ReentrantLock的區別
synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別。既然ReentrantLock是類,那麼它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數,ReentrantLock比synchronized的擴充套件性體現在幾點上:
(1)ReentrantLock可以對獲取鎖的等待時間進行設定,這樣就避免了死鎖
(2)ReentrantLock可以獲取各種鎖的資訊
(3)ReentrantLock可以靈活地實現多路通知
另外,二者的鎖機制其實也是不一樣的:ReentrantLock底層呼叫的是Unsafe的park方法加鎖,synchronized操作的應該是物件頭中mark word。
FutureTask是什麼
這個其實前面有提到過,FutureTask表示一個非同步運算的任務。FutureTask裡面可以傳入一個Callable的具體實現類,可以對這個非同步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作。當然,由於FutureTask也是Runnable介面的實現類,所以FutureTask也可以放入執行緒池中。
一個執行緒如果出現了執行時異常怎麼辦?
如果這個異常沒有被捕獲的話,這個執行緒就停止執行了。另外重要的一點是:如果這個執行緒持有某個某個物件的監視器,那麼這個物件監視器會被立即釋放。
Java當中有哪幾種鎖
-
自旋鎖: 自旋鎖在JDK1.6之後就預設開啟了。基於之前的觀察,共享資料的鎖定狀態只會持續很短的時間,為了這一小段時間而去掛起和恢復執行緒有點浪費,所以這裡就做了一個處理,讓後面請求鎖的那個執行緒在稍等一會,但是不放棄處理器的執行時間,看看持有鎖的執行緒能否快速釋放。為了讓執行緒等待,所以需要讓執行緒執行一個忙迴圈也就是自旋操作。在jdk6之後,引入了自適應的自旋鎖,也就是等待的時間不再固定了,而是由上一次在同一個鎖上的自旋時間及鎖的擁有者狀態來決定。
-
偏向鎖: 在JDK1.之後引入的一項鎖優化,目的是消除資料在無競爭情況下的同步原語。進一步提升程式的執行效能。 偏向鎖就是偏心的偏,意思是這個鎖會偏向第一個獲得他的執行緒,如果接下來的執行過程中,改鎖沒有被其他執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。偏向鎖可以提高帶有同步但無競爭的程式效能,也就是說他並不一定總是對程式執行有利,如果程式中大多數的鎖都是被多個不同的執行緒訪問,那偏向模式就是多餘的,在具體問題具體分析的前提下,可以考慮是否使用偏向鎖。
-
輕量級鎖: 為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裡鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。
如何在兩個執行緒間共享資料
通過線上程之間共享物件就可以了,然後通過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞佇列BlockingQueue就是為執行緒之間共享資料而設計的。
如何正確的使用wait()?使用if還是while?
wait() 方法應該在迴圈呼叫,因為當執行緒獲取到 CPU 開始執行的時候,其他條件可能還沒有滿足,所以在處理前,迴圈檢測條件是否滿足會更好。下面是一段標準的使用 wait 和 notify 方法的程式碼:
synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}
什麼是執行緒區域性變數ThreadLocal
執行緒區域性變數是侷限於執行緒內部的變數,屬於執行緒自身所有,不在多個執行緒間共享。Java提供ThreadLocal類來支援執行緒區域性變數,是一種實現執行緒安全的方式。但是在管理環境下(如 web 伺服器)使用執行緒區域性變數的時候要特別小心,在這種情況下,工作執行緒的生命週期比任何應用變數的生命週期都要長。任何執行緒區域性變數一旦在工作完成後沒有釋放,Java 應用就存在記憶體洩露的風險。
ThreadLoal的作用是什麼?
簡單說ThreadLocal就是一種以空間換時間的做法在每個Thread裡面維護了一個ThreadLocal.ThreadLocalMap把資料進行隔離,資料不共享,自然就沒有執行緒安全方面的問題了。
生產者消費者模型的作用是什麼?
(1)通過平衡生產者的生產能力和消費者的消費能力來提升整個系統的執行效率,這是生產者消費者模型最重要的作用。
(2)解耦,這是生產者消費者模型附帶的作用,解耦意味著生產者和消費者之間的聯絡少,聯絡越少越可以獨自發展而不需要收到相互的制約。
寫一個生產者-消費者佇列
可以通過阻塞佇列實現,也可以通過wait-notify來實現。
使用阻塞佇列來實現
//消費者
public class Producer implements Runnable{
private final BlockingQueue<Integer> queue;
public Producer(BlockingQueue q){
this.queue=q;
}
@Override
public void run() {
try {
while (true){
Thread.sleep(1000);//模擬耗時
queue.put(produce());
}
}catch (InterruptedException e){
}
}
private int produce() {
int n=new Random().nextInt(10000);
System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n);
return n;
}
}
//消費者
public class Consumer implements Runnable {
private final BlockingQueue<Integer> queue;
public Consumer(BlockingQueue q){
this.queue=q;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(2000);//模擬耗時
consume(queue.take());
}catch (InterruptedException e){
}
}
}
private void consume(Integer n) {
System.out.println("Thread:" + Thread.currentThread().getId() + " consume:" + n);
}
}
//測試
public class Main {
public static void main(String[] args) {
BlockingQueue<Integer> queue=new ArrayBlockingQueue<Integer>(100);
Producer p=new Producer(queue);
Consumer c1=new Consumer(queue);
Consumer c2=new Consumer(queue);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
使用wait-notify來實現
該種方式應該最經典,這裡就不做說明了。
如果你提交任務時,執行緒池佇列已滿,這時會發生什麼
如果你使用的LinkedBlockingQueue,也就是無界佇列的話,沒關係,繼續新增任務到阻塞佇列中等待執行,因為LinkedBlockingQueue可以近乎認為是一個無窮大的佇列,可以無限存放任務;如果你使用的是有界佇列比方說ArrayBlockingQueue的話,任務首先會被新增到ArrayBlockingQueue中,ArrayBlockingQueue滿了,則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,預設是AbortPolicy。
為什麼要使用執行緒池
避免頻繁地建立和銷燬執行緒,達到執行緒物件的重用。另外,使用執行緒池還可以根據專案靈活地控制併發的數目。
java中用到的執行緒排程演算法是什麼
搶佔式。一個執行緒用完CPU之後,作業系統會根據執行緒優先順序、執行緒飢餓情況等資料算出一個總的優先順序並分配下一個時間片給某個執行緒執行。
Thread.sleep(0)的作用是什麼
由於Java採用搶佔式的執行緒排程演算法,因此可能會出現某條執行緒常常獲取到CPU控制權的情況,為了讓某些優先順序比較低的執行緒也能獲取到CPU控制權,可以使用Thread.sleep(0)手動觸發一次作業系統分配時間片的操作,這也是平衡CPU控制權的一種操作。
什麼是CAS
CAS,全稱為Compare and Swap,即比較-替換。假設有三個運算元:記憶體值V、舊的預期值A、要修改的值B,當且僅當預期值A和記憶體值V相同時,才會將記憶體值修改為B並返回true,否則什麼都不做並返回false。當然CAS一定要volatile變數配合,這樣才能保證每次拿到的變數是主記憶體中最新的那個值,否則舊的預期值A對某條執行緒來說,永遠是一個不會變的值A,只要某次CAS操作失敗,永遠都不可能成功。
什麼是樂觀鎖和悲觀鎖
樂觀鎖:樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改記憶體中的變數,如果失敗則表示發生衝突,那麼就應該有相應的重試邏輯。
悲觀鎖:悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨佔的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。
ConcurrentHashMap的併發度是什麼?
ConcurrentHashMap的併發度就是segment的大小,預設為16,這意味著最多同時可以有16條執行緒操作ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優勢,任何情況下,Hashtable能同時有兩條執行緒獲取Hashtable中的資料嗎?
ConcurrentHashMap的工作原理
ConcurrentHashMap在jdk 1.6和jdk 1.8實現原理是不同的。
jdk 1.6:
ConcurrentHashMap是執行緒安全的,但是與Hashtablea相比,實現執行緒安全的方式不同。Hashtable是通過對hash表結構進行鎖定,是阻塞式的,當一個執行緒佔有這個鎖時,其他執行緒必須阻塞等待其釋放鎖。ConcurrentHashMap是採用分離鎖的方式,它並沒有對整個hash表進行鎖定,而是區域性鎖定,也就是說當一個執行緒佔有這個區域性鎖時,不影響其他執行緒對hash表其他地方的訪問。
具體實現:ConcurrentHashMap內部有一個Segment.
jdk 1.8
在jdk 8中,ConcurrentHashMap不再使用Segment分離鎖,而是採用一種樂觀鎖CAS演算法來實現同步問題,但其底層還是“陣列+連結串列->紅黑樹”的實現。
CyclicBarrier和CountDownLatch區別
這兩個類非常類似,都在java.util.concurrent下,都可以用來表示程式碼執行到某個點上,二者的區別在於:
-
CyclicBarrier的某個執行緒執行到某個點上之後,該執行緒即停止執行,直到所有的執行緒都到達了這個點,所有執行緒才重新執行;CountDownLatch則不是,某執行緒執行到某個點上之後,只是給某個數值-1而已,該執行緒繼續執行。
-
CyclicBarrier只能喚起一個任務,CountDownLatch可以喚起多個任務
-
CyclicBarrier可重用,CountDownLatch不可重用,計數值為0該CountDownLatch就不可再用了。
java中的++操作符執行緒安全麼?
不是執行緒安全的操作。它涉及到多個指令,如讀取變數值,增加,然後儲存回記憶體,這個過程可能會出現多個執行緒交差。
你有哪些多執行緒開發良好的實踐?
-
給執行緒命名
-
最小化同步範圍
-
優先使用volatile
-
儘可能使用更高層次的併發工具而非wait和notify()來實現執行緒通訊,如BlockingQueue,Semeaphore
-
優先使用併發容器而非同步容器.