本篇用於總結在面試中常見的java問題:

java中equals和等號(==)的區別?

java中的數據類型,可分爲兩類:

1、基本數據類型,也稱原始數據類型。byte,short,char,int,long,float,double,boolean 他們之間的比較,應用雙等號(==),比較的是他們的值。

2、複合數據類型(類),當他們用(==)進行比較的時候,比較的是他們在內存中的存放地址,所以,除非是同一個new出來的對象,他們的比較後的結果爲true,否則比較後結果爲false。JAVA當中所有的類都是繼承於Object這個基類的,在Object中的基類中定義了一個equals的方法,這個方法的初始行爲是比較對象的內存地址,但在一些類庫當中這個方法被覆蓋掉了,如String,Integer,Date在這些類當中equals有其自身的實現,而不再是比較類在堆內存中的存放地址了。 對於複合數據類型之間進行equals比較,在沒有覆寫equals方法的情況下,他們之間的比較還是基於他們在內存中的存放位置的地址值的,因爲Object的equals方法也是用雙等號(==)進行比較的,所以比較後的結果跟雙等號(==)的結果相同。

3、如果在覆寫equals方法的情況下,是用於比較兩個獨立對象的內容是否相同。就好比去比較兩個人的長相是否相同,它比較的兩個對象是獨立的。例如,對於下面的代碼:

  1. String a=new String("foo");

  2. String b=new String("foo");

兩條new語句創建了兩個對象,然後用a,b這兩個變量分別指向了其中一個對象,這是兩個不同的對象,它們的首地址是不同的,即a和b中存儲的數值是不相同的,所以,表達式a==b將返回false,而這兩個對象中的內容是相同的,所以,表達式a.equals(b)將返回true。

int和Integer的區別?

1、Integer是int的包裝類,int則是java的一種基本數據類型

2、Integer變量必須實例化後才能使用,而int變量不需要

3、Integer實際是對象的引用,當new一個Integer時,實際上是生成一個指針指向此對象;而int則是直接存儲數據值

4、Integer的默認值是null,int的默認值是0

延伸:

關於Integer和int的比較

1、由於Integer變量實際上是對一個Integer對象的引用,所以兩個通過new生成的Integer變量永遠是不相等的(因爲new生成的是兩個對象,其內存地址不同)。

  1. Integer i = new Integer(100);
  2. Integer j = new Integer(100);
  3. System.out.print(i == j); //false

2、Integer變量和int變量比較時,只要兩個變量的值是相等的,則結果爲true(因爲包裝類Integer和基本數據類型int比較時,java會自動拆包裝爲int,然後進行比較,實際上就變爲兩個int變量的比較)

  1. Integer i = new Integer(100);
  2. int j = 100;
  3. System.out.print(i == j); //true

3、非new生成的Integer變量和new Integer()生成的變量比較時,結果爲false。(因爲非new生成的Integer變量指向的是java常量池中的對象,而new Integer()生成的變量指向堆中新建的對象,兩者在內存中的地址不同)

  1. Integer i = new Integer(100);
  2. Integer j = 100;
  3. System.out.print(i == j); //false

4、對於兩個非new生成的Integer對象,進行比較時,如果兩個變量的值在區間-128到127之間,則比較結果爲true,如果兩個變量的值不在此區間,則比較結果爲false

  1. Integer i = 100;
  2. Integer j = 100;
  3. System.out.print(i == j); //true
  4. Integer i = 128;
  5. Integer j = 128;
  6. System.out.print(i == j); //false

對於第4條的原因:

java在編譯Integer i = 100 ;時,會翻譯成爲Integer i = Integer.valueOf(100);,而java API中對Integer類型的valueOf的定義如下:

  1. public static Integer valueOf(int i){
  2. assert IntegerCache.high >= 127;
  3. if (i >= IntegerCache.low && i <= IntegerCache.high){
  4. return IntegerCache.cache[i + (-IntegerCache.low)];
  5. }
  6. return new Integer(i);
  7. }

java對於-128到127之間的數,會進行緩存,Integer i = 127時,會將127進行緩存,下次再寫Integer j = 127時,就會直接從緩存中取,就不會new了

JAVA的包裝類、拆箱和裝箱?

雖然 Java 語言是典型的面向對象編程語言,但其中的八種基本數據類型並不支持面向對象編程,基本類型的數據不具備“對象”的特性——不攜帶屬性、沒有方法可調用。 沿用它們只是爲了迎合人類根深蒂固的習慣,並的確能簡單、有效地進行常規數據處理。

這種藉助於非面向對象技術的做法有時也會帶來不便,比如引用類型數據均繼承了 Object 類的特性,要轉換爲 String 類型(經常有這種需要)時只要簡單調用 Object 類中定義的toString()即可,而基本數據類型轉換爲 String 類型則要麻煩得多。爲解決此類問題 ,Java爲每種基本數據類型分別設計了對應的類,稱之爲包裝類(Wrapper Classes),也有教材稱爲外覆類或數據類型類。

基本數據類型及對應的包裝類

每個包裝類的對象可以封裝一個相應的基本類型的數據,並提供了其它一些有用的方法。包裝類對象一經創建,其內容(所封裝的基本類型數據值)不可改變。

基本類型和對應的包裝類可以相互裝換:

  • 由基本類型向對應的包裝類轉換稱爲裝箱,例如把 int 包裝成 Integer 類的對象;
  • 包裝類向對應的基本類型轉換稱爲拆箱,例如把 Integer 類的對象重新簡化爲 int。

包裝類的應用

八個包裝類的使用比較相似,下面是常見的應用場景。

1) 實現 int 和 Integer 的相互轉換

可以通過 Integer 類的構造方法將 int 裝箱,通過 Integer 類的 intValue 方法將 Integer 拆箱。例如:

  1. public class Demo {
  2. public static void main(String[] args) {
  3. int m = 500;
  4. Integer obj = new Integer(m); // 手動裝箱
  5. int n = obj.intValue(); // 手動拆箱
  6. System.out.println("n = " + n);

  7. Integer obj1 = new Integer(500);
  8. System.out.println("obj 等價於 obj1?" + obj.equals(obj1));
  9. }
  10. }

運行結果:

n = 500

obj 等價於 obj1?true

2) 將字符串轉換爲整數

Integer 類有一個靜態的 paseInt() 方法,可以將字符串轉換爲整數,語法爲:

parseInt(String s, int radix);

s 爲要轉換的字符串,radix 爲進制,可選,默認爲十進制。

下面的代碼將會告訴你什麼樣的字符串可以轉換爲整數:

  1. public class Demo {
  2. public static void main(String[] args) {
  3. String str[] = {"123", "123abc", "abc123", "abcxyz"};

  4. for(String str1 : str){
  5. try{
  6. int m = Integer.parseInt(str1, 10);
  7. System.out.println(str1 + " 可以轉換爲整數 " + m);
  8. }catch(Exception e){
  9. System.out.println(str1 + " 無法轉換爲整數");
  10. }
  11. }
  12. }
  13. }

運行結果:

123 可以轉換爲整數 123

123abc 無法轉換爲整數

abc123 無法轉換爲整數

abcxyz 無法轉換爲整數

3) 將整數轉換爲字符串

Integer 類有一個靜態的 toString() 方法,可以將整數轉換爲字符串。例如:

  1. public class Demo {
  2. public static void main(String[] args) {
  3. int m = 500;
  4. String s = Integer.toString(m);
  5. System.out.println("s = " + s);
  6. }
  7. }

運行結果:

s = 500

自動拆箱和裝箱

上面的例子都需要手動實例化一個包裝類,稱爲手動拆箱裝箱。Java 1.5(5.0) 之前必須手動拆箱裝箱。

Java 1.5 之後可以自動拆箱裝箱,也就是在進行基本數據類型和對應的包裝類轉換時,系統將自動進行,這將大大方便程序員的代碼書寫。例如:

  1. public class Demo {
  2. public static void main www.120xh.cn (String[] args) {
  3. int m = 500;
  4. Integer obj = m; // 自動裝箱
  5. int n = obj; // 自動拆箱
  6. System.out.println("n = " + n);

  7. Integer obj1 = 500;
  8. System.out.println("obj 等價於 obj1?" + obj.equals(obj1));
  9. }
  10. }

運行結果:

n = 500

obj 等價於 obj1?true

自動拆箱裝箱是常用的一個功能,需要重點掌握。

什麼是HashCode?

1、hash和hash表是什麼

想要知道這個hashcode,首先得知道hash,通過百度百科看一下:

一些JAVA面試的常見問題彙總

一些JAVA面試的常見問題彙總

hash是一個函數,該函數中的實現就是一種算法,就是通過一系列的算法來得到一個hash值,這個時候,我們就需要知道另一個東西,hash表,通過hash算法得到的hash值就在這張hash表中,也就是說,hash表就是所有的hash值組成的,有很多種hash函數,也就代表着有很多種算法得到hash值,如上面截圖的三種,等會我們就拿第一種來說。

2、hashcode

有了前面的基礎,這裏講解就簡單了,hashcode就是通過hash函數得來的,通俗的說,就是通過某一種算法得到的,hashcode就是在hash表中有對應的位置。

每個對象都有hashcode,對象的hashcode怎麼得來的呢?

首先一個對象肯定有物理地址,在別的博文中會把hashcode說成是代表對象的地址,這裏肯定會讓讀者形成誤區,對象的物理地址跟這個hashcode地址不一樣,hashcode代表對象的地址說的是對象在hash表中的位置,物理地址說的對象存放在內存中的地址,那麼對象如何得到hashcode呢?通過對象的內部地址(也就是物理地址)轉換成一個整數,然後該整數通過hash函數的算法就得到了hashcode,所以,hashcode是什麼呢?就是在hash表中對應的位置。這裏如果還不是很清楚的話,舉個例子,hash表中有 hashcode爲1、hashcode爲2、(...)3、4、5、6、7、8這樣八個位置,有一個對象A,A的物理地址轉換爲一個整數17(這是假如),就通過直接取餘算法,17%8=1,那麼A的hashcode就爲1,且A就在hash表中1的位置。肯定會有其他疑問,接着看下面,這裏只是舉個例子來讓你們知道什麼是hashcode的意義。

3、hashcode的作用

前面說了這麼多關於hash函數,和hashcode是怎麼得來的,還有hashcode對應的是hash表中的位置,可能大家就有疑問,爲什麼hashcode不直接寫物理地址呢,還要另外用一張hash表來代表對象的地址?接下來就告訴你hashcode的作用。

HashCode的存在主要是爲了查找的快捷性,HashCode是用來在散列存儲結構中確定對象的存儲地址的(後半句說的用hashcode來代表對象就是在hash表中的位置)

爲什麼hashcode就查找的更快,比如:我們有一個能存放1000個數這樣大的內存中,在其中要存放1000個不一樣的數字,用最笨的方法,就是存一個數字,就遍歷一遍,看有沒有相同得數,當存了900個數字,開始存901個數字的時候,就需要跟900個數字進行對比,這樣就很麻煩,很是消耗時間,用hashcode來記錄對象的位置,來看一下。hash表中有1、2、3、4、5、6、7、8個位置,存第一個數,hashcode爲1,該數就放在hash表中1的位置,存到100個數字,hash表中8個位置會有很多數字了,1中可能有20個數字,存101個數字時,他先查hashcode值對應的位置,假設爲1,那麼就有20個數字和他的hashcode相同,他只需要跟這20個數字相比較(equals),如果沒一個相同,那麼就放在1這個位置,這樣比較的次數就少了很多,實際上hash表中有很多位置,這裏只是舉例只有8個,所以比較的次數會讓你覺得也挺多的,實際上,如果hash表很大,那麼比較的次數就很少很少了。 通過對原始方法和使用hashcode方法進行對比,我們就知道了hashcode的作用,並且爲什麼要使用hashcode了。

4、equals方法和hashcode的關係

通過前面這個例子,大概可以知道,先通過hashcode來比較,如果hashcode相等,那麼就用equals方法來比較兩個對象是否相等,用個例子說明:上面說的hash表中的8個位置,就好比8個桶,每個桶裏能裝很多的對象,對象A通過hash函數算法得到將它放到1號桶中,當然肯定有別的對象也會放到1號桶中,如果對象B也通過算法分到了1號桶,那麼它如何識別桶中其他對象是否和它一樣呢,這時候就需要equals方法來進行篩選了。

1、如果兩個對象equals相等,那麼這兩個對象的HashCode一定也相同

2、如果兩個對象的HashCode相同,不代表兩個對象就相同,只能說明這兩個對象在散列存儲結構中,存放於同一個位置

這兩條你們就能夠理解了。

5、爲什麼equals方法重寫的話,建議也一起重寫hashcode方法?

舉個例子,其實就明白這個道理了。

比如:有個A類重寫了equals方法,但是沒有重寫hashCode方法,看輸出結果,對象a1和對象a2使用equals方法相等,按照上面的hashcode的用法,那麼他們兩個的hashcode肯定相等,但是這裏由於沒重寫hashcode方法,他們兩個hashcode並不一樣,所以,我們在重寫了equals方法後,儘量也重寫了hashcode方法,通過一定的算法,使他們在equals相等時,也會有相同的hashcode值。

一些JAVA面試的常見問題彙總

實例:現在來看一下String的源碼中的equals方法和hashcode方法。這個類就重寫了這兩個方法,現在爲什麼需要重寫這兩個方法了吧?

equals方法:其實跟我上面寫的那個例子是一樣的原理,所以通過源碼又知道了String的equals方法驗證的是兩個字符串的值是否一樣。還有Double類也重寫了這些方法。很多類有比較這類的,都重寫了這兩個方法,因爲在所有類的父類Object中。equals的功能就是 “==”號的功能。你們還可以比較String對象的equals和==的區別啦。這裏不再說明。

一些JAVA面試的常見問題彙總

hashcode方法

一些JAVA面試的常見問題彙總

6、JAVA多線程

一.線程的生命週期及五種基本狀態

關於Java中線程的生命週期,首先看一下下面這張較爲經典的圖:

一些JAVA面試的常見問題彙總

上圖中基本上囊括了Java中多線程各重要知識點。掌握了上圖中的各知識點,Java中的多線程也就基本上掌握了。主要包括:

Java線程具有五種基本狀態

新建狀態(New):當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();

就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行;

運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;

阻塞狀態(Blocked):處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,纔有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:

1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

2.同步阻塞:線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態;

3.其他阻塞:通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

二. Java多線程的創建及啓動

Java中線程的創建常見有如三種基本形式

1.繼承Thread類,重寫該類的run()方法。

  1. class MyThread extends Thread {

  2. private int i = 0;

  3. @Override
  4. public void run() {
  5. for (i = 0; i < 100; i++) {
  6. System.out.println(Thread.currentThread().getName() + " " + i);
  7. }
  8. }
  9. }
  10. public class ThreadTest {

  11. public static void main(String[] args) {
  12. for (int i = 0; i < 100; i++) {
  13. System.out.println(Thread.currentThread().getName() + " " + i);
  14. if (i == 30) {
  15. Thread myThread1 = new MyThread(); // 創建一個新的線程 myThread1 此線程進入新建狀態
  16. Thread myThread2 = new MyThread(); // 創建一個新的線程 myThread2 此線程進入新建狀態
  17. myThread1.start(); // 調用start()方法使得線程進入就緒狀態
  18. myThread2.start(); // 調用start()方法使得線程進入就緒狀態
  19. }
  20. }
  21. }
  22. }

如上所示,繼承Thread類,通過重寫run()方法定義了一個新的線程類MyThread,其中run()方法的方法體代表了線程需要完成的任務,稱之爲線程執行體。當創建此線程類對象時一個新的線程得以創建,並進入到線程新建狀態。通過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程並不一定會馬上得以執行,這取決於CPU調度時機。

2.實現Runnable接口,並重寫該接口的run()方法,該run()方法同樣是線程執行體,創建Runnable實現類的實例,並以此實例作爲Thread類的target來創建Thread對象,該Thread對象纔是真正的線程對象。

  1. class MyRunnable implements Runnable {
  2. private int i = 0;

  3. @Override
  4. public void run() {
  5. for (i = 0; i < 100; i++) {
  6. System.out.println(Thread.currentThread().getName() + " " + i);
  7. }
  8. }
  9. }
  10. public class ThreadTest {

  11. public static void main(String[] args) {
  12. for (int i = 0; i < 100; i++) {
  13. System.out.println(Thread.currentThread().getName() + " " + i);
  14. if (i == 30) {
  15. Runnable myRunnable = new MyRunnable(); // 創建一個Runnable實現類的對象
  16. Thread thread1 = new Thread(myRunnable); // 將myRunnable作爲Thread target創建新的線程
  17. Thread thread2 = new Thread(myRunnable);
  18. thread1.start(); // 調用start()方法使得線程進入就緒狀態
  19. thread2.start();
  20. }
  21. }
  22. }
  23. }

相信以上兩種創建新線程的方式大家都很熟悉了,那麼Thread和Runnable之間到底是什麼關係呢?我們首先來看一下下面這個例子。

  1. public class ThreadTest {

  2. public static void main(String[] args) {
  3. for (int i = 0; i < 100; i++) {
  4. System.out.println(Thread.currentThread().getName() + " " + i);
  5. if (i == 30) {
  6. Runnable myRunnable = new MyRunnable();
  7. Thread thread = new MyThread(myRunnable);
  8. thread.start();
  9. }
  10. }
  11. }
  12. }

  13. class MyRunnable implements Runnable {
  14. private int i = 0;

  15. @Override
  16. public void run() {
  17. System.out.println("in MyRunnable run");
  18. for (i = 0; i < 100; i++) {
  19. System.out.println(Thread.currentThread().getName() + " " + i);
  20. }
  21. }
  22. }

  23. class MyThread extends Thread {

  24. private int i = 0;

  25. public MyThread(Runnable runnable){
  26. super(runnable);
  27. }

  28. @Override
  29. public void run() {
  30. System.out.println("in MyThread run");
  31. for (i = 0; i < 100; i++) {
  32. System.out.println(Thread.currentThread().getName() + " " + i);
  33. }
  34. }
  35. }

同樣的,與實現Runnable接口創建線程方式相似,不同的地方在於

Thread thread = new MyThread(myRunnable);

那麼這種方式可以順利創建出一個新的線程麼?答案是肯定的。至於此時的線程執行體到底是MyRunnable接口中的run()方法還是MyThread類中的run()方法呢?通過輸出我們知道線程執行體是MyThread類中的run()方法。其實原因很簡單,因爲Thread類本身也是實現了Runnable接口,而run()方法最先是在Runnable接口中定義的方法。

  1. public interface Runnable {

  2. public abstract void run();

  3. }

我們看一下Thread類中對Runnable接口中run()方法的實現:

  1. @Override
  2. public void run() {
  3. if (target != null) {
  4. target.run();
  5. }
  6. }

也就是說,當執行到Thread類中的run()方法時,會首先判斷target是否存在,存在則執行target中的run()方法,也就是實現了Runnable接口並重寫了run()方法的類中的run()方法。但是上述給到的例子中,由於多態的存在,根本就沒有執行到Thread類中的run()方法,而是直接先執行了運行時類型即MyThread類中的run()方法。

3.使用Callable和Future接口創建線程。具體是創建Callable接口的實現類,並實現clall()方法。並使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象作爲Thread對象的target來創建線程。

看着好像有點複雜,直接來看一個例子就清晰了。

  1. public class ThreadTest {

  2. public static void main(String[] args) {

  3. Callable myCallable = new MyCallable(); // 創建MyCallable對象
  4. FutureTask ft = new FutureTask(myCallable); //使用FutureTask來包裝MyCallable對象

  5. for (int i = 0; i < 100; i++) {
  6. System.out.println(Thread.currentThread().getName() + " " + i);
  7. if (i == 30) {
  8. Thread thread = new Thread(ft); //FutureTask對象作爲Thread對象的target創建新的線程
  9. thread.start(); //線程進入到就緒狀態
  10. }
  11. }

  12. System.out.println("主線程for循環執行完畢..");

  13. try {
  14. int sum = ft.get(); //取得新創建的新線程中的call()方法返回的結果
  15. System.out.println("sum = " + sum);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. } catch (ExecutionException e) {
  19. e.printStackTrace();
  20. }

  21. }
  22. }

  23. class MyCallable implements Callable {
  24. private int i = 0;

  25. // 與run()方法不同的是,call()方法具有返回值
  26. @Override
  27. public Integer call() {
  28. int sum = 0;
  29. for (; i < 100; i++) {
  30. System.out.println(Thread.currentThread().getName() + " " + i);
  31. sum += i;
  32. }
  33. return sum;
  34. }

  35. }

首先,我們發現,在實現Callable接口中,此時不再是run()方法了,而是call()方法,此call()方法作爲線程執行體,同時還具有返回值!在創建新的線程時,是通過FutureTask來包裝MyCallable對象,同時作爲了Thread對象的target。那麼看下FutureTask類的定義:

  1. public class FutureTask implements RunnableFuture {

  2. //....

  3. }
  4. public interface RunnableFuture extends Runnable, Future {

  5. void run();

  6. }

於是,我們發現FutureTask類實際上是同時實現了Runnable和Future接口,由此才使得其具有Future和Runnable雙重特性。通過Runnable特性,可以作爲Thread對象的target,而Future特性,使得其可以取得新創建線程中的call()方法的返回值。

執行下此程序,我們發現sum = 4950永遠都是最後輸出的。而“主線程for循環執行完畢..”則很可能是在子線程循環中間輸出。由CPU的線程調度機制,我們知道,“主線程for循環執行完畢..”的輸出時機是沒有任何問題的,那麼爲什麼sum =4950會永遠最後輸出呢?

原因在於通過ft.get()方法獲取子線程call()方法的返回值時,當子線程此方法還未執行完畢,ft.get()方法會一直阻塞,直到call()方法執行完畢才能取到返回值。

上述主要講解了三種常見的線程創建方式,對於線程的啓動而言,都是調用線程對象的start()方法,需要特別注意的是:不能對同一線程對象兩次調用start()方法。

三. Java多線程的就緒、運行和死亡狀態

就緒狀態轉換爲運行狀態:當此線程得到處理器資源;

運行狀態轉換爲就緒狀態:當此線程主動調用yield()方法或在運行過程中失去處理器資源。

運行狀態轉換爲死亡狀態:當此線程線程執行體執行完畢或發生了異常。

此處需要特別注意的是:當調用線程的yield()方法時,線程從運行狀態轉換爲就緒狀態,但接下來CPU調度就緒狀態中的哪個線程具有一定的隨機性,因此,可能會出現A線程調用了yield()方法後,接下來CPU仍然調度了A線程的情況。

由於實際的業務需要,常常會遇到需要在特定時機終止某一線程的運行,使其進入到死亡狀態。目前最通用的做法是設置一boolean型的變量,當條件滿足時,使線程執行體快速執行完畢。如:

  1. 1 public class ThreadTest {
  2. 2
  3. 3 public static void main(String[] args) {
  4. 4
  5. 5 MyRunnable myRunnable = new MyRunnable();
  6. 6 Thread thread = new Thread(myRunnable);
  7. 7
  8. 8 for (int i = 0; i < 100; i++) {
  9. 9 System.out.println(Thread.currentThread().getName() + " " + i);
  10. 10 if (i == 30) {
  11. 11 thread.start();
  12. 12 }
  13. 13 if(i == 40){
  14. 14 myRunnable.stopThread();
  15. 15 }
  16. 16 }
  17. 17 }
  18. 18 }
  19. 19
  20. 20 class MyRunnable implements Runnable {
  21. 21
  22. 22 private boolean stop;
  23. 23
  24. 24 @Override
  25. 25 public void run() {
  26. 26 for (int i = 0; i < 100 && !stop; i++) {
  27. 27 System.out.println(Thread.currentThread().getName() + " " + i);
  28. 28 }
  29. 29 }
  30. 30
  31. 31 public void stopThread() {
  32. 32 this.stop = true;
  33. 33 }
  34. 34
  35. 35 }

7、Java Synchronize 和 Lock 的區別與用法

在分佈式開發中,鎖是線程控制的重要途徑。Java爲此也提供了2種鎖機制,synchronized和lock。做爲Java愛好者,自然少不了對比一下這2種機制,也能從中學到些分佈式開發需要注意的地方。

我們先從最簡單的入手,逐步分析這2種的區別。

一、synchronized和lock的用法區別

synchronized:在需要同步的對象中加入此控制,synchronized可以加在方法上,也可以加在特定代碼塊中,括號中表示需要鎖的對象。

lock:需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做爲鎖,多個線程中必須要使用一個ReentrantLock類做爲對象才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。所以一般會在finally塊中寫unlock()以防死鎖。

用法區別比較簡單,這裏不贅述了,如果不懂的可以看看Java基本語法。

二、synchronized和lock性能區別

synchronized是託管給JVM執行的,而lock是java寫的控制鎖的代碼。在Java1.5中,synchronize是性能低效的。因爲這是一個重量級操作,需要調用操作接口,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock對象,性能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在Java1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化餘地。

說到這裏,還是想提一下這2種機制的具體區別。據我所知,synchronized原始採用的是CPU悲觀鎖機制,即線程獲得的是獨佔鎖。獨佔鎖意味着其他線程只能依靠阻塞來等待線程釋放鎖。而在CPU轉換線程阻塞時會引起線程上下文切換,當有很多線程競爭鎖的時候,會引起CPU頻繁的上下文切換導致效率很低。

而Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。樂觀鎖實現的機制就是CAS操作(Compare and Swap)。我們可以進一步研究ReentrantLock的源代碼,會發現其中比較重要的獲得鎖的一個方法是compareAndSetState。這裏其實就是調用的CPU提供的特殊指令。

現代的CPU提供了指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。這個算法稱作非阻塞算法,意思是一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的算法。

三、synchronized和lock用途區別

synchronized原語和ReentrantLock在一般情況下沒有什麼區別,但是在非常複雜的同步應用中,請考慮使用ReentrantLock,特別是遇到下面2種需求的時候。

1.某個線程在等待一個鎖的控制權的這段時間需要中斷

2.需要分開處理一些wait-notify,ReentrantLock裏面的Condition應用,能夠控制notify哪個線程

3.具有公平鎖功能,每個到來的線程都將排隊等候

先說第一種情況,ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖,這給我們帶來了很大的靈活性。比如:如果A、B兩個線程去競爭鎖,A線程得到了鎖,B線程等待,但是A線程這個時候實在有太多事情要處理,就是一直不返回,B線程可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。這個時候ReentrantLock就提供了2種機制,第一,B線程中斷自己(或者別的線程中斷它),但是ReentrantLock不去響應,繼續讓B線程等待,你再怎麼中斷,我全當耳邊風(synchronized原語就是如此);第二,B線程中斷自己(或者別的線程中斷它),ReentrantLock處理了這個中斷,並且不再等待這個鎖的到來,完全放棄。(如果你沒有了解java的中斷機制,請參考下相關資料,再回頭看這篇文章,80%的人根本沒有真正理解什麼是java的中斷)

這裏來做個試驗,首先搞一個Buffer類,它有讀操作和寫操作,爲了不讀到髒數據,寫和讀都需要加鎖,我們先用synchronized原語來加鎖,如下:


  1. public class Buffer {

  2. private Object lock;

  3. public Buffer() {

  4. lock = this;

  5. }

  6. public void write() {

  7. synchronized (lock) {

  8. long startTime = System.currentTimeMillis();

  9. System.out.println("開始往這個buff寫入數據…");

  10. for (;;)// 模擬要處理很長時間

  11. {

  12. if (System.currentTimeMillis()

  13. - startTime > Integer.MAX_VALUE)

  14. break;

  15. }

  16. System.out.println("終於寫完了");

  17. }

  18. }

  19. public void read() {

  20. synchronized (lock) {

  21. System.out.println("從這個buff讀數據");

  22. }

  23. }

  24. }

接着,我們來定義2個線程,一個線程去寫,一個線程去讀。


  1. public class Writer extends Thread {

  2. private Buffer buff;

  3. public Writer(Buffer buff) {

  4. this.buff = buff;

  5. }

  6. @Override

  7. public void run() {

  8. buff.write();

  9. }

  10. }

  11. public class Reader extends Thread {

  12. private Buffer buff;

  13. public Reader(Buffer buff) {

  14. this.buff = buff;

  15. }

  16. @Override

  17. public void run() {

  18. buff.read();//這裏估計會一直阻塞

  19. System.out.println("讀結束");

  20. }

  21. }

好了,寫一個Main來試驗下,我們有意先去“寫”,然後讓“讀”等待,“寫”的時間是無窮的,就看“讀”能不能放棄了。


  1. public class Test {

  2. public static void main(String[] args) {

  3. Buffer buff = new Buffer();

  4. final Writer writer = new Writer(buff);

  5. final Reader reader = new Reader(buff);

  6. writer.start();

  7. reader.start();

  8. new Thread(new Runnable() {

  9. @Override

  10. public void run() {

  11. long start = System.currentTimeMillis();

  12. for (;;) {

  13. //等5秒鐘去中斷讀

  14. if (System.currentTimeMillis()

  15. - start > 5000) {

  16. System.out.println("不等了,嘗試中斷");

  17. reader.interrupt();

  18. break;

  19. }

  20. }

  21. }

  22. }).start();

  23. }

  24. }

我們期待“讀”這個線程能退出等待鎖,可是事與願違,一旦讀這個線程發現自己得不到鎖,就一直開始等待了,就算它等死,也得不到鎖,因爲寫線程要21億秒才能完成 T_T ,即使我們中斷它,它都不來響應下,看來真的要等死了。這個時候,ReentrantLock給了一種機制讓我們來響應中斷,讓“讀”能伸能屈,勇敢放棄對這個鎖的等待。我們來改寫Buffer這個類,就叫BufferInterruptibly吧,可中斷緩存。

  1. import java.util.concurrent.locks.ReentrantLock;

  2. public class BufferInterruptibly {

  3. private ReentrantLock lock = new ReentrantLock();

  4. public void write() {

  5. lock.lock();

  6. try {

  7. long startTime = System.currentTimeMillis();

  8. System.out.println("開始往這個buff寫入數據…");

  9. for (;;)// 模擬要處理很長時間

  10. {

  11. if (System.currentTimeMillis()

  12. - startTime > Integer.MAX_VALUE)

  13. break;

  14. }

  15. System.out.println("終於寫完了");

  16. } finally {

  17. lock.unlock();

  18. }

  19. }

  20. public void read() throws InterruptedException {

  21. lock.lockInterruptibly();// 注意這裏,可以響應中斷

  22. try {

  23. System.out.println("從這個buff讀數據");

  24. } finally {

  25. lock.unlock();

  26. }

  27. }

  28. }

當然,要對reader和writer做響應的修改


  1. public class Reader extends Thread {

  2. private BufferInterruptibly buff;

  3. public Reader(BufferInterruptibly buff) {

  4. this.buff = buff;

  5. }

  6. @Override

  7. public void run() {

  8. try {

  9. buff.read();//可以收到中斷的異常,從而有效退出

  10. } catch (InterruptedException e) {

  11. System.out.println("我不讀了");

  12. }

  13. System.out.println("讀結束");

  14. }

  15. }

  16. /**

  17. * Writer倒不用怎麼改動

  18. */

  19. public class Writer extends Thread {

  20. private BufferInterruptibly buff;

  21. public Writer(BufferInterruptibly buff) {

  22. this.buff = buff;

  23. }

  24. @Override

  25. public void run() {

  26. buff.write();

  27. }

  28. }

  29. public class Test {

  30. public static void main(String[] args) {

  31. BufferInterruptibly buff = new BufferInterruptibly();

  32. final Writer writer = new Writer(buff);

  33. final Reader reader = new Reader(buff);

  34. writer.start();

  35. reader.start();

  36. new Thread(new Runnable() {

  37. @Override

  38. public void run() {

  39. long start = System.currentTimeMillis();

  40. for (;;) {

  41. if (System.currentTimeMillis()

  42. - start > 5000) {

  43. System.out.println("不等了,嘗試中斷");

  44. reader.interrupt();

  45. break;

  46. }

  47. }

  48. }

  49. }).start();

  50. }

  51. }

這次“讀”線程接收到了lock.lockInterruptibly()中斷,並且有效處理了這個“異常”。

至於第二種情況,ReentrantLock可以與Condition的配合使用,Condition爲ReentrantLock鎖的等待和釋放提供控制邏輯。

例如,使用ReentrantLock加鎖之後,可以通過它自身的Condition.await()方法釋放該鎖,線程在此等待Condition.signal()方法,然後繼續執行下去。await方法需要放在while循環中,因此,在不同線程之間實現併發控制,還需要一個volatile的變量,boolean是原子性的變量。因此,一般的併發控制的操作邏輯如下所示:


  1. volatile boolean isProcess = false;

  2. ReentrantLock lock = new ReentrantLock();

  3. Condtion processReady = lock.newCondtion();

  4. thread: run() {

  5. lock.lock();

  6. isProcess = true;

  7. try {

  8. while(!isProcessReady) { //isProcessReady 是另外一個線程的控制變量

  9. processReady.await();//釋放了lock,在此等待signal

  10. }catch (InterruptedException e) {

  11. Thread.currentThread().interrupt();

  12. } finally {

  13. lock.unlock();

  14. isProcess = false;

  15. }

  16. }

  17. }

  18. }

這裏只是代碼使用的一段簡化,下面我們看Hadoop的一段摘取的源碼:


  1. private class MapOutputBuffer

  2. implements MapOutputCollector, IndexedSortable {

  3. ...

  4. boolean spillInProgress;

  5. final ReentrantLock spillLock = new ReentrantLock();

  6. final Condition spillDone = spillLock.newCondition();

  7. final Condition spillReady = spillLock.newCondition();

  8. volatile boolean spillThreadRunning = false;

  9. final SpillThread spillThread = new SpillThread();

  10. ...

  11. public MapOutputBuffer(TaskUmbilicalProtocol umbilical, JobConf job,

  12. TaskReporter reporter

  13. ) throws IOException, ClassNotFoundException {

  14. ...

  15. spillInProgress = false;

  16. spillThread.setDaemon(true);

  17. spillThread.setName("SpillThread");

  18. spillLock.lock();

  19. try {

  20. spillThread.start();

  21. while (!spillThreadRunning) {

  22. spillDone.await();

  23. }

  24. } catch (InterruptedException e) {

  25. throw new IOException("Spill thread failed to initialize", e);

  26. } finally {

  27. spillLock.unlock();

  28. }

  29. }

  30. protected class SpillThread extends Thread {

  31. @Override

  32. public void run() {

  33. spillLock.lock();

  34. spillThreadRunning = true;

  35. try {

  36. while (true) {

  37. spillDone.signal();

  38. while (!spillInProgress) {

  39. spillReady.await();

  40. }

  41. try {

  42. spillLock.unlock();

  43. sortAndSpill();

  44. } catch (Throwable t) {

  45. sortSpillException = t;

  46. } finally {

  47. spillLock.lock();

  48. if (bufend < bufstart) {

  49. bufvoid = kvbuffer.length;

  50. }

  51. kvstart = kvend;

  52. bufstart = bufend;

  53. spillInProgress = false;

  54. }

  55. }

  56. } catch (InterruptedException e) {

  57. Thread.currentThread().interrupt();

  58. } finally {

  59. spillLock.unlock();

  60. spillThreadRunning = false;

  61. }

  62. }

  63. }

代碼中spillDone 就是 spillLock的一個newCondition()。調用spillDone.await()時可以釋放spillLock鎖,線程進入阻塞狀態,而等待其他線程的 spillDone.signal()操作時,就會喚醒線程,重新持有spillLock鎖。

這裏可以看出,利用lock可以使我們多線程交互變得方便,而使用synchronized則無法做到這點。

最後呢,ReentrantLock這個類還提供了2種競爭鎖的機制:公平鎖和非公平鎖。這2種機制的意思從字面上也能瞭解個大概:即對於多線程來說,公平鎖會依賴線程進來的順序,後進來的線程後獲得鎖。而非公平鎖的意思就是後進來的鎖也可以和前邊等待鎖的線程同時競爭鎖資源。對於效率來講,當然是非公平鎖效率更高,因爲公平鎖還要判斷是不是線程隊列的第一個纔會讓線程獲得鎖。

相关文章