本文已經收錄自筆者開源的 JavaGuide: https://github.com/Snailclimb (【Java學習+面試指南】 一份涵蓋大部分Java程序員所須要掌握的核心知識)若是以爲不錯的還,不妨去點個Star,鼓勵一下!
首先回顧一下在程序設計語言中有關將參數傳遞給方法(或函數)的一些專業術語。按值調用(call by value)表示方法接收的是調用者提供的值,而按引用調用(call by reference)表示方法接收的是調用者提供的變量地址。一個方法能夠修改傳遞引用所對應的變量值,而不能修改傳遞值調用所對應的變量值。 它用來描述各類程序設計語言(不僅是 Java)中方法參數傳遞方式。html
Java 程序設計語言老是採用按值調用。也就是說,方法獲得的是全部參數值的一個拷貝,也就是說,方法不能修改傳遞給它的任何參數變量的內容。java
下面經過 3 個例子來給你們說明c++
example 1
public static void main(String[] args) { int num1 = 10; int num2 = 20; swap(num1, num2); System.out.println("num1 = " + num1); System.out.println("num2 = " + num2); } public static void swap(int a, int b) { int temp = a; a = b; b = temp; System.out.println("a = " + a); System.out.println("b = " + b); }
結果:git
a = 20 b = 10 num1 = 10 num2 = 20
解析:程序員
在 swap 方法中,a、b 的值進行交換,並不會影響到 num一、num2。由於,a、b 中的值,只是從 num一、num2 的複製過來的。也就是說,a、b 至關於 num一、num2 的副本,副本的內容不管怎麼修改,都不會影響到原件自己。github
經過上面例子,咱們已經知道了一個方法不能修改一個基本數據類型的參數,而對象引用做爲參數就不同,請看 example2.面試
example 2
public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; System.out.println(arr[0]); change(arr); System.out.println(arr[0]); } public static void change(int[] array) { // 將數組的第一個元素變爲0 array[0] = 0; }
結果:算法
1 0
解析:spring
array 被初始化 arr 的拷貝也就是一個對象的引用,也就是說 array 和 arr 指向的是同一個數組對象。 所以,外部對引用對象的改變會反映到所對應的對象上。數據庫
經過 example2 咱們已經看到,實現一個改變對象參數狀態的方法並非一件難事。理由很簡單,方法獲得的是對象引用的拷貝,對象引用及其餘的拷貝同時引用同一個對象。
不少程序設計語言(特別是,C++和 Pascal)提供了兩種參數傳遞的方式:值調用和引用調用。有些程序員(甚至本書的做者)認爲 Java 程序設計語言對對象採用的是引用調用,實際上,這種理解是不對的。因爲這種誤解具備必定的廣泛性,因此下面給出一個反例來詳細地闡述一下這個問題。
example 3
public class Test { public static void main(String[] args) { // TODO Auto-generated method stub Student s1 = new Student("小張"); Student s2 = new Student("小李"); Test.swap(s1, s2); System.out.println("s1:" + s1.getName()); System.out.println("s2:" + s2.getName()); } public static void swap(Student x, Student y) { Student temp = x; x = y; y = temp; System.out.println("x:" + x.getName()); System.out.println("y:" + y.getName()); } }
結果:
x:小李 y:小張 s1:小張 s2:小李
解析:
交換以前:
交換以後:
經過上面兩張圖能夠很清晰的看出: 方法並無改變存儲在變量 s1 和 s2 中的對象引用。swap 方法的參數 x 和 y 被初始化爲兩個對象引用的拷貝,這個方法交換的是這兩個拷貝
總結
Java 程序設計語言對對象採用的不是引用調用,實際上,對象引用是按
值傳遞的。
下面再總結一下 Java 中方法參數的使用狀況:
參考:
《Java 核心技術卷 Ⅰ》基礎知識第十版第四章 4.5 小節
== : 它的做用是判斷兩個對象的地址是否是相等。即,判斷兩個對象是否是同一個對象。(基本數據類型==比較的是值,引用數據類型==比較的是內存地址)
equals() : 它的做用也是判斷兩個對象是否相等。但它通常有兩種使用狀況:
舉個例子:
public class test1 { public static void main(String[] args) { String a = new String("ab"); // a 爲一個引用 String b = new String("ab"); // b爲另外一個引用,對象的內容同樣 String aa = "ab"; // 放在常量池中 String bb = "ab"; // 從常量池中查找 if (aa == bb) // true System.out.println("aa==bb"); if (a == b) // false,非同一對象 System.out.println("a==b"); if (a.equals(b)) // true System.out.println("aEQb"); if (42 == 42.0) { // true System.out.println("true"); } } }
說明:
面試官可能會問你:「你重寫過 hashcode 和 equals 麼,爲何重寫 equals 時必須重寫 hashCode 方法?」
hashCode() 的做用是獲取哈希碼,也稱爲散列碼;它其實是返回一個 int 整數。這個哈希碼的做用是肯定該對象在哈希表中的索引位置。hashCode() 定義在 JDK 的 Object.java 中,這就意味着 Java 中的任何類都包含有 hashCode() 函數。另外須要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 語言或 c++ 實現的,該方法一般用來將對象的 內存地址 轉換爲整數以後返回。
/** * Returns a hash code value for the object. This method is * supported for the benefit of hash tables such as those provided by * {@link java.util.HashMap}. * <p> * As much as is reasonably practical, the hashCode method defined by * class {@code Object} does return distinct integers for distinct * objects. (This is typically implemented by converting the internal * address of the object into an integer, but this implementation * technique is not required by the * Java™ programming language.) * * @return a hash code value for this object. * @see java.lang.Object#equals(java.lang.Object) * @see java.lang.System#identityHashCode */ public native int hashCode();
散列表存儲的是鍵值對(key-value),它的特色是:能根據「鍵」快速的檢索出對應的「值」。這其中就利用到了散列碼!(能夠快速找到所須要的對象)
咱們以「HashSet 如何檢查重複」爲例子來講明爲何要有 hashCode:
當你把對象加入 HashSet 時,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,同時也會與其餘已經加入的對象的 hashcode 值做比較,若是沒有相符的 hashcode,HashSet 會假設對象沒有重複出現。可是若是發現有相同 hashcode 值的對象,這時會調用 equals()方法來檢查 hashcode 相等的對象是否真的相同。若是二者相同,HashSet 就不會讓其加入操做成功。若是不一樣的話,就會從新散列到其餘位置。(摘自個人 Java 啓蒙書《Head fist java》第二版)。這樣咱們就大大減小了 equals 的次數,相應就大大提升了執行速度。
在這裏解釋一位小夥伴的問題。如下內容摘自《Head Fisrt Java》。
由於 hashCode() 所使用的雜湊算法也許恰好會讓多個對象傳回相同的雜湊值。越糟糕的雜湊算法越容易碰撞,但這也與數據值域分佈的特性有關(所謂碰撞也就是指的是不一樣的對象獲得相同的 hashCode)。
咱們剛剛也提到了 HashSet,若是 HashSet 在對比的時候,一樣的 hashcode 有多個對象,它會使用 equals() 來判斷是否真的相同。也就是說 hashcode 只是用來縮小查找成本。
可變性
簡單的來講:String 類中使用 final 關鍵字修飾字符數組來保存字符串,private final char value[]
,因此 String 對象是不可變的。而 StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數組保存字符串char[]value
可是沒有用 final 關鍵字修飾,因此這兩種對象都是可變的。
StringBuilder 與 StringBuffer 的構造方法都是調用父類構造方法也就是 AbstractStringBuilder 實現的,你們能夠自行查閱源碼。
AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; AbstractStringBuilder() { } AbstractStringBuilder(int capacity) { value = new char[capacity]; }
線程安全性
String 中的對象是不可變的,也就能夠理解爲常量,線程安全。AbstractStringBuilder 是 StringBuilder 與 StringBuffer 的公共父類,定義了一些字符串的基本操做,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 對方法加了同步鎖或者對調用的方法加了同步鎖,因此是線程安全的。StringBuilder 並無對方法進行加同步鎖,因此是非線程安全的。
性能
每次對 String 類型進行改變的時候,都會生成一個新的 String 對象,而後將指針指向新的 String 對象。StringBuffer 每次都會對 StringBuffer 對象自己進行操做,而不是生成新的對象並改變對象引用。相同狀況下使用 StringBuilder 相比使用 StringBuffer 僅能得到 10%~15% 左右的性能提高,但卻要冒多線程不安全的風險。
對於三者使用的總結:
簡單來講就是 String 類利用了 final 修飾的 char 類型數組存儲字符,源碼以下圖因此:
/** The value is used for character storage. */ private final char value[];
我以爲若是別人問這個問題的話,回答不可變就能夠了。
下面只是給你們看兩個有表明性的例子:
1) String 不可變但不表明引用不能夠變
String str = "Hello"; str = str + " World"; System.out.println("str=" + str);
結果:
str=Hello World
解析:
實際上,原來 String 的內容是不變的,只是 str 由原來指向"Hello"的內存地址轉爲指向"Hello World"的內存地址而已,也就是說多開闢了一塊內存區域給"Hello World"字符串。
2) 經過反射是能夠修改所謂的「不可變」對象
// 建立字符串"Hello World", 並賦給引用s String s = "Hello World"; System.out.println("s = " + s); // Hello World // 獲取String類中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); // 改變value屬性的訪問權限 valueFieldOfString.setAccessible(true); // 獲取s對象上的value屬性的值 char[] value = (char[]) valueFieldOfString.get(s); // 改變value所引用的數組中的第5個字符 value[5] = '_'; System.out.println("s = " + s); // Hello_World
結果:
s = Hello World s = Hello_World
解析:
用反射能夠訪問私有成員, 而後反射出 String 對象中的 value 屬性, 進而改變經過得到的 value 引用改變數組的結構。可是通常咱們不會這麼作,這裏只是簡單提一下有這個東西。
JAVA 反射機制是在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱爲 java 語言的反射機制。
反射是框架設計的靈魂。
在咱們平時的項目開發過程當中,基本上不多會直接使用到反射機制,但這不能說明反射機制沒有用,實際上有不少設計、開發都與反射機制有關,例如模塊化的開發,經過反射去調用對應的字節碼;動態代理設計模式也採用了反射機制,還有咱們平常使用的 Spring/Hibernate 等框架也大量使用到了反射機制。
舉例:① 咱們在使用 JDBC 鏈接數據庫時使用 Class.forName()
經過反射加載數據庫的驅動程序;②Spring 框架也用到不少反射機制,最經典的就是 xml 的配置模式。Spring 經過 XML 配置模式裝載 Bean 的過程:1) 將程序內全部 XML 或 Properties 配置文件加載入內存中;
2)Java 類裏面解析 xml 或 properties 裏面的內容,獲得對應實體類的字節碼字符串以及相關的屬性信息; 3)使用反射機制,根據這個字符串得到某個類的 Class 實例; 4)動態配置實例的屬性
推薦閱讀:
Java 虛擬機(JVM)是運行 Java 字節碼的虛擬機。JVM 有針對不一樣系統的特定實現(Windows,Linux,macOS),目的是使用相同的字節碼,它們都會給出相同的結果。
什麼是字節碼?採用字節碼的好處是什麼?
在 Java 中,JVM 能夠理解的代碼就叫作字節碼
(即擴展名爲.class
的文件),它不面向任何特定的處理器,只面向虛擬機。Java 語言經過字節碼的方式,在必定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特色。因此 Java 程序運行時比較高效,並且,因爲字節碼並不針對一種特定的機器,所以,Java 程序無須從新編譯即可在多種不一樣操做系統的計算機上運行。
Java 程序從源代碼到運行通常有下面 3 步:
咱們須要格外注意的是 .class->機器碼 這一步。在這一步 JVM 類加載器首先加載字節碼文件,而後經過解釋器逐行解釋執行,這種方式的執行速度會相對比較慢。並且,有些方法和代碼塊是常常須要被調用的(也就是所謂的熱點代碼),因此後面引進了 JIT 編譯器,而 JIT 屬於運行時編譯。當 JIT 編譯器完成第一次編譯後,其會將字節碼對應的機器碼保存下來,下次能夠直接使用。而咱們知道,機器碼的運行效率確定是高於 Java 解釋器的。這也解釋了咱們爲何常常會說 Java 是編譯與解釋共存的語言。
HotSpot 採用了惰性評估(Lazy Evaluation)的作法,根據二八定律,消耗大部分系統資源的只有那一小部分的代碼(熱點代碼),而這也就是 JIT 所須要編譯的部分。JVM 會根據代碼每次被執行的狀況收集信息並相應地作出一些優化,所以執行的次數越多,它的速度就越快。JDK 9 引入了一種新的編譯模式 AOT(Ahead of Time Compilation),它是直接將字節碼編譯成機器碼,這樣就避免了 JIT 預熱等各方面的開銷。JDK 支持分層編譯和 AOT 協做使用。可是 ,AOT 編譯器的編譯質量是確定比不上 JIT 編譯器的。
總結:
Java 虛擬機(JVM)是運行 Java 字節碼的虛擬機。JVM 有針對不一樣系統的特定實現(Windows,Linux,macOS),目的是使用相同的字節碼,它們都會給出相同的結果。字節碼和不一樣系統的 JVM 實現是 Java 語言「一次編譯,隨處能夠運行」的關鍵所在。
JDK 是 Java Development Kit,它是功能齊全的 Java SDK。它擁有 JRE 所擁有的一切,還有編譯器(javac)和工具(如 javadoc 和 jdb)。它可以建立和編譯程序。
JRE 是 Java 運行時環境。它是運行已編譯 Java 程序所需的全部內容的集合,包括 Java 虛擬機(JVM),Java 類庫,java 命令和其餘的一些基礎構件。可是,它不能用於建立新程序。
若是你只是爲了運行一下 Java 程序的話,那麼你只須要安裝 JRE 就能夠了。若是你須要進行一些 Java 編程方面的工做,那麼你就須要安裝 JDK 了。可是,這不是絕對的。有時,即便您不打算在計算機上進行任何 Java 開發,仍然須要安裝 JDK。例如,若是要使用 JSP 部署 Web 應用程序,那麼從技術上講,您只是在應用程序服務器中運行 Java 程序。那你爲何須要 JDK 呢?由於應用程序服務器會將 JSP 轉換爲 Java servlet,而且須要使用 JDK 來編譯 servlet。
先看下 java 中的編譯器和解釋器:
Java 中引入了虛擬機的概念,即在機器和編譯程序之間加入了一層抽象的虛擬的機器。這臺虛擬的機器在任何平臺上都提供給編譯程序一個的共同的接口。編譯程序只須要面向虛擬機,生成虛擬機可以理解的代碼,而後由解釋器來將虛擬機代碼轉換爲特定系統的機器碼執行。在 Java 中,這種供虛擬機理解的代碼叫作字節碼
(即擴展名爲.class
的文件),它不面向任何特定的處理器,只面向虛擬機。每一種平臺的解釋器是不一樣的,可是實現的虛擬機是相同的。Java 源程序通過編譯器編譯後變成字節碼,字節碼由虛擬機解釋執行,虛擬機將每一條要執行的字節碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼,而後在特定的機器上運行。這也就是解釋了 Java 的編譯與解釋並存的特色。
Java 源代碼---->編譯器---->jvm 可執行的 Java 字節碼(即虛擬指令)---->jvm---->jvm 中解釋器----->機器可執行的二進制機器碼---->程序運行。
採用字節碼的好處:
Java 語言經過字節碼的方式,在必定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特色。因此 Java 程序運行時比較高效,並且,因爲字節碼並不專對一種特定的機器,所以,Java 程序無須從新編譯即可在多種不一樣的計算機上運行。
注意:Java8 後接口能夠有默認實現( default )。
發生在同一個類中,方法名必須相同,參數類型不一樣、個數不一樣、順序不一樣,方法返回值和訪問修飾符能夠不一樣。
下面是《Java 核心技術》對重載這個概念的介紹:
重寫是子類對父類的容許訪問的方法的實現過程進行從新編寫,發生在子類中,方法名、參數列表必須相同,返回值範圍小於等於父類,拋出的異常範圍小於等於父類,訪問修飾符範圍大於等於父類。另外,若是父類方法訪問修飾符爲 private 則子類就不能重寫該方法。也就是說方法提供的行爲改變,而方法的外貌並無改變。
封裝把一個對象的屬性私有化,同時提供一些能夠被外界訪問的屬性的方法,若是屬性不想被外界訪問,咱們大可沒必要提供方法給外界訪問。可是若是一個類沒有提供給外界訪問的方法,那麼這個類也沒有什麼意義了。
繼承是使用已存在的類的定義做爲基礎創建新類的技術,新類的定義能夠增長新的數據或新的功能,也能夠用父類的功能,但不能選擇性地繼承父類。經過使用繼承咱們可以很是方便地複用之前的代碼。
關於繼承以下 3 點請記住:
所謂多態就是指程序中定義的引用變量所指向的具體類型和經過該引用變量發出的方法調用在編程時並不肯定,而是在程序運行期間才肯定,即一個引用變量到底會指向哪一個類的實例對象,該引用變量發出的方法調用究竟是哪一個類中實現的方法,必須在由程序運行期間才能決定。
在 Java 中有兩種形式能夠實現多態:繼承(多個子類對同一方法的重寫)和接口(實現接口並覆蓋接口中同一方法)。
進程是程序的一次執行過程,是系統運行程序的基本單位,所以進程是動態的。系統運行一個程序便是一個進程從建立,運行到消亡的過程。
在 Java 中,當咱們啓動 main 函數時其實就是啓動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。
以下圖所示,在 windows 中經過查看任務管理器的方式,咱們就能夠清楚看到 window 當前運行的進程(.exe 文件的運行)。
線程與進程類似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程當中能夠產生多個線程。與進程不一樣的是同類的多個線程共享進程的堆和方法區資源,但每一個線程有本身的程序計數器、虛擬機棧和本地方法棧,因此係統在產生一個線程,或是在各個線程之間做切換工做時,負擔要比進程小得多,也正由於如此,線程也被稱爲輕量級進程。
Java 程序天生就是多線程程序,咱們能夠經過 JMX 來看一下一個普通的 Java 程序有哪些線程,代碼以下。
public class MultiThread { public static void main(String[] args) { // 獲取 Java 線程管理 MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不須要獲取同步的 monitor 和 synchronizer 信息,僅獲取線程和線程堆棧信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 遍歷線程信息,僅打印線程 ID 和線程名稱信息 for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } } }
上述程序輸出以下(輸出內容可能不一樣,不用太糾結下面每一個線程的做用,只用知道 main 線程執行 main 方法便可):
[5] Attach Listener //添加事件 [4] Signal Dispatcher // 分發處理給 JVM 信號的線程 [3] Finalizer //調用對象 finalize 方法的線程 [2] Reference Handler //清除 reference 線程 [1] main //main 線程,程序入口
從上面的輸出內容能夠看出:一個 Java 程序的運行是 main 線程和多個其餘線程同時運行。
從 JVM 角度說進程和線程之間的關係
下圖是 Java 內存區域,經過下圖咱們從 JVM 的角度來講一下線程和進程之間的關係。若是你對 Java 內存區域 (運行時數據區) 這部分知識不太瞭解的話能夠閱讀一下這篇文章:《多是把 Java 內存區域講的最清楚的一篇文章》
<div align="center">
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/JVM運行時數據區域.png" width="600px"/>
</div>
從上圖能夠看出:一個進程中能夠有多個線程,多個線程共享進程的堆和方法區 (JDK1.8 以後的元空間)資源,可是每一個線程有本身的程序計數器、虛擬機棧 和 本地方法棧。
總結: 線程 是 進程 劃分紅的更小的運行單位。線程和進程最大的不一樣在於基本上各進程是獨立的,而各線程則不必定,由於同一進程中的線程極有可能會相互影響。線程執行開銷小,但不利於資源的管理和保護;而進程正相反
下面是該知識點的擴展內容!
下面來思考這樣一個問題:爲何程序計數器、虛擬機棧和本地方法棧是線程私有的呢?爲何堆和方法區是線程共享的呢?
程序計數器主要有下面兩個做用:
須要注意的是,若是執行的是 native 方法,那麼程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的纔是下一條指令的地址。
因此,程序計數器私有主要是爲了線程切換後能恢復到正確的執行位置。
因此,爲了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。
堆和方法區是全部線程共享的資源,其中堆是進程中最大的一塊內存,主要用於存放新建立的對象 (全部對象都在這裏分配內存),方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
多線程編程中通常線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能獲得有效執行,CPU 採起的策略是爲每一個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會從新處於就緒狀態讓給其餘線程使用,這個過程就屬於一次上下文切換。
歸納來講就是:當前任務在執行完 CPU 時間片切換到另外一個任務以前會先保存本身的狀態,以便下次再切換回這個任務時,能夠再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。
上下文切換一般是計算密集型的。也就是說,它須要至關可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都須要納秒量級的時間。因此,上下文切換對系統來講意味着消耗大量的 CPU 時間,事實上,多是操做系統中時間消耗最大的操做。
Linux 相比與其餘操做系統(包括其餘類 Unix 系統)有不少的優勢,其中有一項就是,其上下文切換和模式切換的時間消耗很是少。
多個線程同時被阻塞,它們中的一個或者所有都在等待某個資源被釋放。因爲線程被無限期地阻塞,所以程序不可能正常終止。
以下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,因此這兩個線程就會互相等待而進入死鎖狀態。
下面經過一個例子來講明線程死鎖,代碼模擬了上圖的死鎖的狀況 (代碼來源於《併發編程之美》):
public class DeadLockDemo { private static Object resource1 = new Object();//資源 1 private static Object resource2 = new Object();//資源 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "線程 1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "線程 2").start(); } }
Output
Thread[線程 1,5,main]get resource1 Thread[線程 2,5,main]get resource2 Thread[線程 1,5,main]waiting get resource2 Thread[線程 2,5,main]waiting get resource1
線程 A 經過 synchronized (resource1) 得到 resource1 的監視器鎖,而後經過Thread.sleep(1000);
讓線程 A 休眠 1s 爲的是讓線程 B 獲得執行而後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,而後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。
學過操做系統的朋友都知道產生死鎖必須具有如下四個條件:
咱們只要破壞產生死鎖的四個條件中的其中一個就能夠了。
破壞互斥條件
這個條件咱們沒有辦法破壞,由於咱們用鎖原本就是想讓他們互斥的(臨界資源須要互斥訪問)。
破壞請求與保持條件
一次性申請全部的資源。
破壞不剝奪條件
佔用部分資源的線程進一步申請其餘資源時,若是申請不到,能夠主動釋放它佔有的資源。
破壞循環等待條件
靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。
咱們對線程 2 的代碼修改爲下面這樣就不會產生死鎖了。
new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "線程 2").start();
Output
Thread[線程 1,5,main]get resource1 Thread[線程 1,5,main]waiting get resource2 Thread[線程 1,5,main]get resource2 Thread[線程 2,5,main]get resource1 Thread[線程 2,5,main]waiting get resource2 Thread[線程 2,5,main]get resource2 Process finished with exit code 0
咱們分析一下上面的代碼爲何避免了死鎖的發生?
線程 1 首先得到到 resource1 的監視器鎖,這時候線程 2 就獲取不到了。而後線程 1 再去獲取 resource2 的監視器鎖,能夠獲取到。而後線程 1 釋放了對 resource一、resource2 的監視器鎖的佔用,線程 2 獲取到就能夠執行了。這樣就破壞了破壞循環等待條件,所以避免了死鎖。
這是另外一個很是經典的 java 多線程面試問題,並且在面試中會常常被問到。很簡單,可是不少人都會答不上來!
new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啓動一個線程並使線程進入了就緒狀態,當分配到時間片後就能夠開始運行了。 start() 會執行線程的相應準備工做,而後自動執行 run() 方法的內容,這是真正的多線程工做。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,因此這並非多線程工做。
總結: 調用 start 方法方可啓動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,仍是在主線程裏執行。
做者的其餘開源項目推薦: