1.什么是Java虛擬機?為什么Java被稱作是“平台無關的編程語言”?
Java 虛擬機是一個可以執行 Java 字節碼的虛擬機進程。Java 源文件被編譯成能被 Java 虛擬機執行的字節碼文件。
Java 被設計成允許應用程序可以運行在任意的平台,而不需要程序員為每一個平台單獨重寫或者是重新編譯。
Java 虛擬機讓這個變為可能,因為它知道底層硬件平台的指令長度和其他特性。
2.JDK和JRE的區別是什么?
JDK:java開發工具包,包含了JRE、編譯器和其它工具(如:javaDOc、java調試器)
JRE:java運行環境,包含java虛擬機和java程序所需的核心類庫。
如果只是想跑java程序,那么只需安裝JRE,如果要寫java程序並且運行,那就需要JDK了。
3."static"關鍵字是什么意思?Java中是否可以覆蓋一個private或者是static的方法?
如果一個類的變量或者方法前面有static修飾,那么表明這個方法或者變量屬於這個類,也就是說可以在不創建對象的情況下直接使用
當父類的方法被private修飾時,表明該方法為父類私有,對其他任何類都是不可見的,因此如果子類定了一個與父類一樣的方法,這對於子類來說相當於是一個新的私有方法,且如果要進行向上轉型,然后去調用該“覆蓋方法”,會產生編譯錯誤
class Parent { private fun() { ... } } class Child extends Parent { private fun() { ... } } class Test { public static void main(String[] args) { Parent c = new Child(); c.fun(); //編譯出錯 } }
static方法時編譯時靜態綁定的,屬於類,而覆蓋是運行時動態綁定的(動態綁定的多態),因此不能覆蓋.
4.Java支持的基本數據類型有哪些?什么是自動拆裝箱?
java支持的基本數據類型有以下9種:byte,shot,int,long,float,double,char,boolean,void.
自動拆裝箱是java從jdk1.5引用,目的是將原始類型自動的裝換為相對應的對象,也可以逆向進行,即拆箱。這也體現java中一切皆對象的宗旨。
所謂自動裝箱就是將原始類型自動的轉換為對應的對象,而拆箱就是將對象類型轉換為基本類型。java中的自動拆裝箱通常發生在變量賦值的過程中,如:
Integer object = 3; //自動裝箱 int o = object; //拆箱
在java中,應該注意自動拆裝箱,因為有時可能因為java自動裝箱機制,而導致創建了許多對象,對於內存小的平台會造成壓力。
5. 覆蓋和重載是什么?
覆蓋也叫重寫,發生在子類與父類之間,表示子類中的方法可以與父類中的某個方法的名稱和參數完全相同,通過子類創建的實例對象調用這個方法時,將調用子類中的定義方法,這相當於把父類中定義的那個完全相同的方法給覆蓋了,這也是面向對象編程的多態性的一種表現。
重載是指在一個類中,可以有多個相同名稱的方法,但是他們的參數列表的個數或類型不同,當調用該方法時,根據傳遞的參數類型調用對應參數列表的方法。當參數列表相同但返回值不同時,將會出現編譯錯誤,這並不是重載,因為jvm無法根據返回值類型來判斷應該調用哪個方法。
6.Java支持多繼承么?如果不支持,如何實現?
在java中是單繼承的,也就是說一個類只能繼承一個父類。
java中實現多繼承有兩種方式,一是接口,而是內部類.
//實現多個接口 如果兩個接口的變量相同 那么在調用該變量的時候 編譯出錯 interface interface1 { static String field = "dd"; public void fun1(); } interface interface2 { static String field = "dddd"; public void fun2(); } class child implements interface1,interface2 { static String field = "dddd"; @Override public void fun2() { } @Override public void fun1() { } } //內部類 間接多繼承 class Child { class Father { private void strong() { System.out.println("父類"); } } class Mother { public void getCute() { System.out.println("母親"); } } public void getStrong() { Father f = new Father(); f.strong(); } public void getCute() { Mother m = new Mother(); m.getCute(); } }
7.什么是值傳遞和引用傳遞?java中是值傳遞還是引用傳遞,還是都有?
值傳遞就是在方法調用的時候,實參是將自己的一份拷貝賦給形參,在方法內,對該參數值的修改不影響原來實參,常見的例子就是剛開始學習c語言的時候那個交換方法的例子了。
引用傳遞是在方法調用的時候,實參將自己的地址傳遞給形參,此時方法內對該參數值的改變,就是對該實參的實際操作。
在java中只有一種傳遞方式,那就是值傳遞.可能比較讓人迷惑的就是java中的對象傳遞時,對形參的改變依然會影響到該對象的內容。
下面這個例子來說明java中是值傳遞.
public class Test { public static void main(String[] args) { StringBuffer sb = new StringBuffer("hello "); getString(sb); System.out.println(sb); } public static void getString(StringBuffer s) { //s = new StringBuffer("ha"); s.append("world"); } }
在上面這個例子中,當前輸出結果為:hello world。這並沒有什么問題,可能就是大家平常所理解的引用傳遞,那么當然會改變StringBuffer的內容。但是如果把上面的注釋去掉,那么就會輸出:hello.此時sb的值並沒有變成ha hello. 假如說是引用傳遞的話,那么形參的s也就是sb的地址,此時在方法里new StringBuffer(),並將該對象賦給s,也就是說s現在指向了這個新創建的對象.按照引用傳遞的說法,此時對s的改變就是對sb的操作,也就是說sb應該也指向新創建的對象,那么輸出的結果應該為ha world.但實際上輸出的僅是hello.這說明sb指向的還是原來的對象,而形參s指向的才是創建的對象,這也就驗證了java中的對象傳遞也是值傳遞。
8.接口和抽象類的區別是什么?
不同點在於:
- 接口中所有的方法隱含的都是抽象的。而抽象類則可以同時包含抽象和非抽象的方法。
- 類可以實現很多個接口,但是只能繼承一個抽象類
- 類如果要實現一個接口,它必須要實現接口聲明的所有方法。但是,類可以不實現抽象類聲明的所有方法,當然,在這種情況下,類也必須得聲明成是抽象的。
- 抽象類可以在不提供接口方法實現的情況下實現接口。
- Java 接口中聲明的變量默認都是 final 的。抽象類可以包含非 final 的變量。
- Java 接口中的成員函數默認是 public 的。抽象類的成員函數可以是 private,protected 或者是 public 。
- 接口是絕對抽象的,不可以被實例化(java 8已支持在接口中實現默認的方法)。抽象類也不可以被實例化,但是,如果它包含 main 方法的話是可以被調用的。
9.構造器(constructor)是否可被重寫(override)?
構造方法是不能被子類重寫的,但是構造方法可以重載,也就是說一個類可以有多個構造方法。
10.Math.round(11.5) 等於多少? Math.round(-11.5)等於多少?
Math.round(11.5)==12 Math.round(-11.5)==-11 round 方法返回與參數 最接近的長整數,參數加 1/2 后求其 floor.
11. String, StringBuffer StringBuilder的區別。
tring 的長度是不可變的;
StringBuffer的長度是可變的,如果你對字符串中的內容經常進行操作,特別是內容要修改時,那么使用 StringBuffer,如果最后需要 >String,那么使用 StringBuffer 的 toString() 方法;線程安全;
StringBuilder 是從 JDK 5 開始,為StringBuffer該類補充了一個單個線程使用的等價類;通常應該優先使用 StringBuilder 類,因>為它支持所有相同的操作,但由於它不執行同步,所以速度更快。
使用字符串的時候要特別小心,如果對一個字符串要經常改變的話,就一定不要用String,否則會創建許多無用的對象出來.
來看一下比較
String s = "hello"+"world"+"i love you"; StringBuffer Sb = new StringBuilder("hello").append("world").append("i love you");
這個時候s有多個字符串進行拼接,按理來說會有多個對象產生,但是jvm會對此進行一個優化,也就是說只創建了一個對象,此時它的執行速度要比StringBuffer拼接快.再看下面這個:
String s2 = "hello"; String s3 = "world"; String s4 = "i love you"; String s1 = s2 + s3 + s4;
上面這種情況,就會多創建出來三個對象,造成了內存空間的浪費.
12.JVM內存分哪幾個區,每個區的作用是什么?
java虛擬機主要分為以下一個區:
方法區:
- 有時候也成為永久代,在該區內很少發生垃圾回收,但是並不代表不發生GC,在這里進行的GC主要是對方法區里的常量池和對類型的卸載
- 方法區主要用來存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯后的代碼等數據。
- 該區域是被線程共享的。
- 方法區里有一個運行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池具有動態性,也就是說常量並不一定是編譯時確定,運行時生成的常量也會存在這個常量池中。
虛擬機棧:
- 虛擬機棧也就是我們平常所稱的棧內存,它為java方法服務,每個方法在執行的時候都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接和方法出口等信息。
- 虛擬機棧是線程私有的,它的生命周期與線程相同。
- 局部變量表里存儲的是基本數據類型、returnAddress類型(指向一條字節碼指令的地址)和對象引用,這個對象引用有可能是指向對象起始地址的一個指針,也有可能是代表對象的句柄或者與對象相關聯的位置。局部變量所需的內存空間在編譯器間確定
4.操作數棧的作用主要用來存儲運算結果以及運算的操作數,它不同於局部變量表通過索引來訪問,而是壓棧和出棧的方式
5.每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接.動態鏈接就是將常量池中的符號引用在運行期轉化為直接引用。
本地方法棧
本地方法棧和虛擬機棧類似,只不過本地方法棧為Native方法服務。堆
java堆是所有線程所共享的一塊內存,在虛擬機啟動時創建,幾乎所有的對象實例都在這里創建,因此該區域經常發生垃圾回收操作。
程序計數器
內存空間小,字節碼解釋器工作時通過改變這個計數值可以選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理和線程恢復等功能都需要依賴這個計數器完成。該內存區域是唯一一個java虛擬機規范沒有規定任何OOM情況的區域。
13.如和判斷一個對象是否存活?(或者GC對象的判定方法)
判斷一個對象是否存活有兩種方法:
- 引用計數法
所謂引用計數法就是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器為零時,說明此對象沒有被引用,也就是“死對象”,將會被垃圾回收.
引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象A引用對象B,對象B又引用者對象A,那么此時A,B對象的引用計數器都不為零,也就造成無法完成垃圾回收,所以主流的虛擬機都沒有采用這種算法。
2.可達性算法(引用鏈法)
該算法的思想是:從一個被稱為GC Roots的對象開始向下搜索,如果一個對象到GC Roots沒有任何引用鏈相連時,則說明此對象不可用。
在java中可以作為GC Roots的對象有以下幾種:
- 虛擬機棧中引用的對象
- 方法區類靜態屬性引用的對象
- 方法區常量池引用的對象
- 本地方法棧JNI引用的對象
雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,一個對象比不一定會被回收。當一個對象不可達GC Root時,這個對象並
不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收需要經歷兩次標記
如果對象在可達性分析中沒有與GC Root的引用鏈,那么此時就會被第一次標記並且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者已被虛擬機調用過,那么就認為是沒必要的。
如果該對象有必要執行finalize()方法,那么這個對象將會放在一個稱為F-Queue的對隊列中,虛擬機會觸發一個Finalize()線程去執行,此線程是低優先級的,並且虛擬機不會承諾一直等待它運行完,這是因為如果finalize()執行緩慢或者發生了死鎖,那么就會造成F-Queue隊列一直等待,造成了內存回收系統的崩潰。GC對處於F-Queue中的對象進行第二次被標記,這時,該對象將被移除"即將回收"集合,等待回收。
14.簡述java垃圾回收機制?
在java中,程序員是不需要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在JVM中,有一個垃圾回收線程,它是低優先級的,在正常情況下是不會執行的,只有在虛擬機空閑或者當前堆內存不足時,才會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。
15.java中垃圾收集的方法有哪些?
- 標記-清除:
這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的對象,然后統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,導致以后程序在分配較大的對象時,由於沒有充足的連續內存而提前觸發一次GC動作。- 復制算法:
為了解決效率問題,復制算法將可用內存按容量划分為相等的兩部分,然后每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象復制到第二塊內存上,然后一次性清楚完第一塊內存,再將第二塊上的對象復制到第一塊。但是這種方式,內存的代價太高,每次基本上都要浪費一般的內存。
於是將該算法進行了改進,內存區域不再是按照1:1去划分,而是將內存划分為8:1:1三部分,較大那份內存交Eden區,其余是兩塊較小的內存區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象復制到第二塊內存區上,然后清除Eden區,如果此時存活的對象太多,以至於Survivor不夠時,會將這些對象通過分配擔保機制復制到老年代中。(java堆又分為新生代和老年代)- 標記-整理
該算法主要是為了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了復制算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然后清除掉端邊界以外的對象,這樣就不會產生內存碎片了。
分代收集
現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存周期,將堆分為新生代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那么這時就采用復制算法。老年代里的對象存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理或者 標記-清除。
16.java內存模型
java內存模型(JMM)是線程間通信的控制機制.JMM定義了主內存和線程之間抽象關系。線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。Java內存模型的抽象示意圖如下:
![]()
從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:
- 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
- 然后,線程B到主內存中去讀取線程A之前已更新過的共享變量。
寫的很好:http://www.infoq.com/cn/articles/java-memory-model-1
17.java類加載過程?
java類加載需要經歷一下7個過程:
加載
加載時類加載的第一個過程,在這個階段,將完成一下三件事情:
- 通過一個類的全限定名獲取該類的二進制流。
- 將該二進制流中的靜態存儲結構轉化為方法去運行時數據結構。
- 在內存中生成該類的Class對象,作為該類的數據訪問入口。
驗證
驗證的目的是為了確保Class文件的字節流中的信息不回危害到虛擬機.在該階段主要完成以下四鍾驗證:
- 文件格式驗證:驗證字節流是否符合Class文件的規范,如主次版本號是否在當前虛擬機范圍內,常量池中的常量是否有不被支持的類型.
- 元數據驗證:對字節碼描述的信息進行語義分析,如這個類是否有父類,是否集成了不被繼承的類等。
- 字節碼驗證:是整個驗證過程中最復雜的一個階段,通過驗證數據流和控制流的分析,確定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉指令是否正確等。
- 符號引用驗證:這個動作在后面的解析過程中發生,主要是為了確保解析動作能正確執行。
准備
准備階段是為類的靜態變量分配內存並將其初始化為默認值,這些內存都將在方法區中進行分配。准備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。
public static int value=123;//在准備階段value初始值為0 。在初始化階段才會變為123 。
解析
該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之前,也有可能在初始化之后。
初始化
初始化時類加載的最后一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。
18. 簡述java類加載機制?
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,解析和初始化,最終形成可以被虛擬機直接使用的java類型。
19. 類加載器雙親委派模型機制?
當一個類收到了類加載請求時,不會自己先去加載這個類,而是將其委派給父類,由父類去加載,如果此時父類不能加載,反饋給子類,由子類去完成類的加載。
20.什么是類加載器,類加載器有哪些?
實現通過類的權限定名獲取該類的二進制字節流的代碼塊叫做類加載器。
主要有一下四種類加載器:
- 啟動類加載器(Bootstrap ClassLoader)用來加載java核心類庫,無法被java程序直接引用。
- 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找並加載 Java 類。
- 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
- 用戶自定義類加載器,通過繼承 java.lang.ClassLoader類的方式實現。
21.簡述java內存分配與回收策率以及Minor GC和Major GC
- 對象優先在堆的Eden區分配。
- 大對象直接進入老年代.
- 長期存活的對象將直接進入老年代.
當Eden區沒有足夠的空間進行分配時,虛擬機會執行一次Minor GC.Minor Gc通常發生在新生代的Eden區,在這個區的對象生存期短,往往發生Gc的頻率較高,回收速度比較快;Full Gc/Major GC 發生在老年代,一般情況下,觸發老年代GC的時候不會觸發Minor GC,但是通過配置,可以在Full GC之前進行一次Minor GC這樣可以加快老年代的回收速度。
22.HashMap的工作原理是什么?
HashMap內部是通過一個數組實現的,只是這個數組比較特殊,數組里存儲的元素是一個Entry實體(jdk 8為Node),這個Entry實體主要包含key、value以及一個指向自身的next指針。HashMap是基於hashing實現的,當我們進行put操作時,根據傳遞的key值得到它的hashcode,然后再用這個hashcode與數組的長度進行模運算,得到一個int值,就是Entry要存儲在數組的位置(下標);當通過get方法獲取指定key的值時,會根據這個key算出它的hash值(數組下標),根據這個hash值獲取數組下標對應的Entry,然后判斷Entry里的key,hash值或者通過equals()比較是否與要查找的相同,如果相同,返回value,否則的話,遍歷該鏈表(有可能就只有一個Entry,此時直接返回null),直到找到為止,否則返回null。
HashMap之所以在每個數組元素存儲的是一個鏈表,是為了解決hash沖突問題,當兩個對象的hash值相等時,那么一個位置肯定是放不下兩個值的,於是hashmap采用鏈表來解決這種沖突,hash值相等的兩個元素會形成一個鏈表。
23.HashMap與HashTable的區別是什么?
1.HashTable基於Dictionary類,而HashMap是基於AbstractMap。Dictionary是任何可將鍵映射到相應值的類的抽象父類,而AbstractMap是基於Map接口的實現,它以最大限度地減少實現此接口所需的工作。(在java 8中我查看源碼發現Hashtable並沒有繼承Dictionary,而且里面也沒有同步方法,是不是java 8中Hashtable不在同步的了?有沒有人解釋一下?)
- HashMap的key和value都允許為null,而Hashtable的key和value都不允許為null。HashMap遇到key為null的時候,調用putForNullKey方法進行處理,而對value沒有處理;Hashtable遇到null,直接返回NullPointerException。
- Hashtable是同步的,而HashMap是非同步的,但是我們也可以通過Collections.synchronizedMap(hashMap),使其實現同步。
24.CorrentHashMap的工作原理?
jdk 1.6版:ConcurrenHashMap可以說是HashMap的升級版,ConcurrentHashMap是線程安全的,但是與Hashtablea相比,實現線程安全的方式不同。Hashtable是通過對hash表結構進行鎖定,是阻塞式的,當一個線程占有這個鎖時,其他線程必須阻塞等待其釋放鎖。ConcurrentHashMap是采用分離鎖的方式,它並沒有對整個hash表進行鎖定,而是局部鎖定,也就是說當一個線程占有這個局部鎖時,不影響其他線程對hash表其他地方的訪問。
具體實現:ConcurrentHashMap內部有一個Segment<K,V>數組,該Segment對象可以充當鎖。Segment對象內部有一個HashEntry<K,V>數組,於是每個Segment可以守護若干個桶(HashEntry),每個桶又有可能是一個HashEntry連接起來的鏈表,存儲發生碰撞的元素。
每個ConcurrentHashMap在默認並發級下會創建包含16個Segment對象的數組,每個數組有若干個桶,當我們進行put方法時,通過hash方法對key進行計算,得到hash值,找到對應的segment,然后對該segment進行加鎖,然后調用segment的put方法進行存儲操作,此時其他線程就不能訪問當前的segment,但可以訪問其他的segment對象,不會發生阻塞等待。
jdk 1.8版在jdk 8中,ConcurrentHashMap不再使用Segment分離鎖,而是采用一種樂觀鎖CAS算法來實現同步問題,但其底層還是“數組+鏈表->紅黑樹”的實現。
25.遍歷一個List有哪些不同的方式?
List<String> strList = new ArrayList<>(); //for-each for(String str:strList) { System.out.print(str); } //use iterator 盡量使用這種 更安全(fail-fast) Iterator<String> it = strList.iterator(); while(it.hasNext) { System.out.printf(it.next()); }
26.fail-fast與fail-safe有什么區別?
Iterator的fail-fast屬性與當前的集合共同起作用,因此它不會受到集合中任何改動的影響。Java.util包中的所有集合類都被設計為fail->fast的,而java.util.concurrent中的集合類都為fail-safe的。當檢測到正在遍歷的集合的結構被改變時,Fail-fast迭代器拋出ConcurrentModificationException,而fail-safe迭代器從不拋出ConcurrentModificationException。
27.Array和ArrayList有何區別?什么時候更適合用Array?
- Array可以容納基本類型和對象,而ArrayList只能容納對象。
- Array是指定大小的,而ArrayList大小是固定的
28.哪些集合類提供對元素的隨機訪問?
ArrayList、HashMap、TreeMap和HashTable類提供對元素的隨機訪問。
29.HashSet的底層實現是什么?
通過看源碼知道HashSet的實現是依賴於HashMap的,HashSet的值都是存儲在HashMap中的。在HashSet的構造法中會初始化一個HashMap對象,HashSet不允許值重復,因此,HashSet的值是作為HashMap的key存儲在HashMap中的,當存儲的值已經存在時返回false。
30.LinkedHashMap的實現原理?
LinkedHashMap也是基於HashMap實現的,不同的是它定義了一個Entry header,這個header不是放在Table里,它是額外獨立出來的。LinkedHashMap通過繼承hashMap中的Entry,並添加兩個屬性Entry before,after,和header結合起來組成一個雙向鏈表,來實現按插入順序或訪問順序排序。LinkedHashMap定義了排序模式accessOrder,該屬性為boolean型變量,對於訪問順序,為true;對於插入順序,則為false。一般情況下,不必指定排序模式,其迭代順序即為默認為插入順序。
31.LinkedList和ArrayList的區別是什么?
- ArrayList是基於數組實現,LinkedList是基於鏈表實現
- ArrayList在查找時速度快,LinkedList在插入與刪除時更具優勢
32.什么是線程?進程和線程的關系是什么?
線程可定義為進程內的一個執行單位,或者定義為進程內的一個可調度實體。 在具有多線程機制的操作系統中,處理機調度的基本單位不是進程而是線程。一個進程可以有多個線程,而且至少有一個可執行線程。
打個比喻:進程好比工廠(計算機)里的車間,一個工廠里有多個車間(進程)在運轉,每個車間里有多個工人(線程)在協同工作,這些工人就可以理解為線程。
線程和進程的關系:
- 線程是進程的一個組成部分.
- 進程的多個線程都在進程地址空間活動.
- 系統資源是分配給進程的,線程需要資源時,系統從進程的資源里分配給線程.
- 處理機調度的基本單位是線程.
33.Thread 類中的start() 和 run() 方法有什么區別?
start()方法被用來啟動新創建的線程,而且start()內部調用了run()方法,這和直接調用run()方法的效果不一樣。當你調用run()方法的時候,只會是在原來的線程中調用,沒有新的線程啟動,start()方法才會啟動新線程。
34.什么是線程安全?
當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為。
線程安全的核心是“正確性”,也就是說當多個線程訪問某個類時,能夠得到預期的結果,那么就是線程安全的。
35.Java中有哪幾種鎖?
自旋鎖:自旋鎖在JDK1.6之后就默認開啟了。基於之前的觀察,共享數據的鎖定狀態只會持續很短的時間,為了這一小段時間而去掛起和恢復線程有點浪費,所以這里就做了一個處理,讓后面請求鎖的那個線程在稍等一會,但是不放棄處理器的執行時間,看看持有鎖的線程能否快速釋放。為了讓線程等待,所以需要讓線程執行一個忙循環也就是自旋操作。
在jdk6之后,引入了自適應的自旋鎖,也就是等待的時間不再固定了,而是由上一次在同一個鎖上的自旋時間及鎖的擁有者狀態來決定
偏向鎖:在JDK1.之后引入的一項鎖優化,目的是消除數據在無競爭情況下的同步原語。進一步提升程序的運行性能。 偏向鎖就是偏心的偏,意思是這個鎖會偏向第一個獲得他的線程,如果接下來的執行過程中,改鎖沒有被其他線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。偏向鎖可以提高帶有同步但無競爭的程序性能,也就是說他並不一定總是對程序運行有利,如果程序中大多數的鎖都是被多個不同的線程訪問,那偏向模式就是多余的,在具體問題具體分析的前提下,可以考慮是否使用偏向鎖。
輕量級鎖:為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6里鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率,下文會詳細分析
36.synchronized內置鎖
java中以synchronize的形式,為防止資源沖突提供了內置支持。當任務要執行被synchronize關鍵字保護的代碼段時,它將檢查鎖是否可用,然后獲取鎖——執行代碼——釋放鎖。
所有對象都自動含有單一的鎖。當一個線程正在訪問一個對象的synchronized方法,那么其他線程不能訪問該對象的其他synchronized方法,但可以訪問非synchronized方法。因為一個對象只有一把鎖,當一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法。
synchronized代碼塊
synchronized(synObject) { }
當在某個線程中執行這段代碼塊,該線程會獲取對象synObject的鎖,從而使得其他線程無法同時訪問該代碼塊。synObject可以是this,代表獲取當前對象的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。
針對每一個類,也有一個鎖,所以static synchronize 方法可以在類的范圍內防止對static數據的並發訪問。如果一個線程執行一個對象的非static synchronized方法,另外一個線程需要執行這個對象所屬類的static synchronized方法,此時不會發生互斥現象,因為訪問static synchronized方法占用的是類鎖,而訪問非static synchronized方法占用的是對象鎖,所以不存在互斥現象。
對於synchronized方法或者synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程占用的鎖,因此不會由於異常導致出現死鎖現象。
37.ThreadLocal理解
ThreadLocal是一個創建線程局部變量的類。通常情況下我們創建的變量,可以被多個線程訪問並修改,通過ThreadLocal創建的變量只能被當前線程訪問。
ThreadLocal內部實現
ThreadLocal提供了set和get方法.
set方法會先獲取當前線程,然后用當前線程作為句柄,獲取ThreadLocaMap對象,並判斷該對象是否為空,如果為空則創建一個,並設置值,不為空則直接設置值。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
ThreadLocal的值是放入了當前線程的一個ThreadLocalMap實例中,所以只能在本線程中訪問,其他線程無法訪問。
ThreadLocal並不會導致內存泄露,因為ThreadLocalMap中的key存儲的是ThreadLocal實例的弱引用,因此如果應用使用了線程池,即便之前的線程實例處理完之后出於復用的目的依然存活,也不會產生內存泄露。
38.為什么wait, notify 和 notifyAll這些方法不在thread類里面?
這是個設計相關的問題,它考察的是面試者對現有系統和一些普遍存在但看起來不合理的事物的看法。回答這些問題的時候,你要說明為什么把這些方法放在Object類里是有意義的,還有不把它放在Thread類里的原因。一個很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需要等待某些鎖那么調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因為鎖屬於對象。
作者:littleKang
鏈接:https://www.jianshu.com/p/04c0d796d877
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。