CodeGym /Java Blog /Toto sisi /Java Core 的前 50 個工作面試問題和答案。第2部分
John Squirrels
等級 41
San Francisco

Java Core 的前 50 個工作面試問題和答案。第2部分

在 Toto sisi 群組發布
Java Core 的前 50 個工作面試問題和答案。第1部分Java Core 的前 50 個工作面試問題和答案。 第 2 - 1 部分

多線程

24. 如何在 Java 中創建新線程?

不管怎樣,線程是使用 Thread 類創建的。但是有多種方法可以做到這一點……
  1. 繼承java.lang.Thread
  2. 實現java.lang.Runnable接口——Thread的構造函數接受一個 Runnable 對象。
讓我們談談他們每個人。

繼承線程類

在這種情況下,我們讓我們的類繼承java.lang.Thread。它有一個run()方法,而這正是我們所需要的。新線程的所有生命和邏輯都將在這個方法中。它有點像新線程的主要方法。之後,剩下的就是創建我們類的對象並調用start()方法。這將創建一個新線程並開始執行其邏輯。讓我們來看看:

/**
* An example of how to create threads by inheriting the {@link Thread} class.
*/
class ThreadInheritance extends Thread {

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName());
   }

   public static void main(String[] args) {
       ThreadInheritance threadInheritance1 = new ThreadInheritance();
       ThreadInheritance threadInheritance2 = new ThreadInheritance();
       ThreadInheritance threadInheritance3 = new ThreadInheritance();
       threadInheritance1.start();
       threadInheritance2.start();
       threadInheritance3.start();
   }
}
控制台輸出將是這樣的:
線程 1 線程 0 線程 2
也就是說,即使在這裡我們也看到線程不是按順序執行的,而是按照 JVM 認為適合運行它們的順序執行的:)

實現Runnable接口

如果您反對繼承和/或已經繼承了其他一些類,則可以使用java.lang.Runnable接口。在這裡,我們讓我們的類通過實現run()方法來實現這個接口,就像上面的例子一樣。剩下的就是創建Thread對象。似乎代碼行越多越糟糕。但是我們知道繼承是多麼有害,最好無論如何都要避免它;)看看:

/**
* An example of how to create threads from the {@link Runnable} interface.
* It's easier than easy — we implement this interface and then pass an instance of our object
* to the constructor.
*/
class ThreadInheritance implements Runnable {

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName());
   }

   public static void main(String[] args) {
       ThreadInheritance runnable1 = new ThreadInheritance();
       ThreadInheritance runnable2 = new ThreadInheritance();
       ThreadInheritance runnable3 = new ThreadInheritance();

       Thread threadRunnable1 = new Thread(runnable1);
       Thread threadRunnable2 = new Thread(runnable2);
       Thread threadRunnable3 = new Thread(runnable3);

       threadRunnable1.start();
       threadRunnable2.start();
       threadRunnable3.start();
   }
}
結果如下:
線程 0 線程 1 線程 2

25.進程和線程有什麼區別?

Java Core 的前 50 個工作面試問題和答案。 第 2 - 2 部分進程和線程在以下方面有所不同:
  1. 正在運行的程序稱為進程,而線程是進程的一部分。
  2. 進程是獨立的,但線程是進程的一部分。
  3. 進程在內存中有不同的地址空間,但線程共享一個公共地址空間。
  4. 線程之間的上下文切換比進程之間的切換更快。
  5. 進程間通信比線程間通信更慢且更昂貴。
  6. 父進程中的任何更改都不會影響子進程,但父線程中的更改會影響子線程。

26.多線程有什麼好處?

  1. 多線程允許應用程序/程序始終響應輸入,即使它已經在運行一些後台任務;
  2. 多線程可以更快地完成任務,因為線程是獨立運行的;
  3. 多線程可以更好地利用高速緩存,因為線程可以訪問共享內存資源;
  4. 多線程減少了所需的服務器數量,因為一台服務器可以同時運行多個線程。

27.線程的生命週期有哪些狀態?

Java Core 的前 50 個工作面試問題和答案。 第 2 - 3 部分
  1. New:在這個狀態下,Thread對像是用new操作符創建的,但是一個新的線程還不存在。在我們調用start()方法之前,線程不會啟動。
  2. Runnable:在這種狀態下,線程在start()之後就可以運行了 方法被調用。但是,它還沒有被線程調度器選中。
  3. 運行:在這種狀態下,線程調度程序從就緒狀態中選擇一個線程,並運行它。
  4. Waiting/Blocked:在這種狀態下,線程沒有運行,但它仍然存在或正在等待另一個線程完成。
  5. Dead/Terminated:當線程退出run()方法時,它處於死或終止狀態。

28. 是否可以運行一個線程兩次?

不行,我們不能重啟一個線程,因為一個線程啟動運行後,就進入了Dead狀態。如果我們確實嘗試啟動一個線程兩次,則會拋出java.lang.IllegalThreadStateException 。讓我們來看看:

class DoubleStartThreadExample extends Thread {

   /**
    * Simulate the work of a thread
    */
   public void run() {
	// Something happens. At this state, this is not essential.
   }

   /**
    * Start the thread twice
    */
   public static void main(String[] args) {
       DoubleStartThreadExample doubleStartThreadExample = new DoubleStartThreadExample();
       doubleStartThreadExample.start();
       doubleStartThreadExample.start();
   }
}
一旦執行到同一個線程的第二次啟動,就會出現異常。親自嘗試一下 ;) 看到它一次比聽到它一百次要好。

29.如果不調用start()而直接調用run()會怎麼樣?

是的,你當然可以調用run()方法,但是不會創建新線程,並且該方法不會在單獨的線程上運行。在這種情況下,我們有一個調用普通方法的普通對象。如果我們談論的是start()方法,那就是另一回事了。調用此方法時,JVM會啟動一個新線程。該線程依次調用我們的方法 ;) 不相信嗎?在這裡,試一試:

class ThreadCallRunExample extends Thread {

   public void run() {
       for (int i = 0; i < 5; i++) {
           System.out.print(i);
       }
   }

   public static void main(String args[]) {
       ThreadCallRunExample runExample1 = new ThreadCallRunExample();
       ThreadCallRunExample runExample2 = new ThreadCallRunExample();

       // Two ordinary methods will be called in the main thread, one after the other.
       runExample1.run();
       runExample2.run();
   }
}
控制台輸出將如下所示:
0123401234
如您所見,沒有創建線程。一切都像在普通課堂上一樣。首先,第一個對象的方法被執行,然後是第二個。

30.什麼是守護線程?

守護線程是一個以比另一個線程低的優先級執行任務的線程。換句話說,它的工作是執行輔助任務,這些任務只需要與另一個(主)線程一起完成。有很多自動運行的守護線程,比如垃圾收集器、終結器等。

為什麼 Java 會終止守護線程?

守護線程的唯一目的是為用戶線程提供後台支持。因此,如果主線程終止,則 JVM 會自動終止其所有守護線程。

Thread 類的方法

java.lang.Thread類提供兩種使用守護線程的方法:
  1. public void setDaemon(boolean status) — 此方法指示這是否將是守護線程。默認值為false。這意味著除非您明確說明,否則不會創建守護線程。
  2. public boolean isDaemon() — 此方法本質上是守護進程變量的 getter ,我們使用前面的方法設置它。
例子:

class DaemonThreadExample extends Thread {

   public void run() {
       // Checks whether this thread is a daemon
       if (Thread.currentThread().isDaemon()) {
           System.out.println("daemon thread");
       } else {
           System.out.println("user thread");
       }
   }

   public static void main(String[] args) {
       DaemonThreadExample thread1 = new DaemonThreadExample();
       DaemonThreadExample thread2 = new DaemonThreadExample();
       DaemonThreadExample thread3 = new DaemonThreadExample();

       // Make thread1 a daemon thread.
       thread1.setDaemon(true);

       System.out.println("daemon? " + thread1.isDaemon());
       System.out.println("daemon? " + thread2.isDaemon());
       System.out.println("daemon? " + thread3.isDaemon());

       thread1.start();
       thread2.start();
       thread3.start();
   }
}
控制台輸出:
守護進程?真正的守護進程?假守護進程?false 守護線程 用戶線程 用戶線程
從輸出中,我們看到在線程本身內部,我們可以使用靜態currentThread()方法來找出它是哪個線程。或者,如果我們有線程對象的引用,我們也可以直接從中查找。這提供了必要級別的可配置性。

31. 是否可以在線程創建後使其成為守護進程?

不會。如果您嘗試這樣做,您將得到一個IllegalThreadStateException。這意味著我們只能在它啟動之前創建一個守護線程。例子:

class SetDaemonAfterStartExample extends Thread {

   public void run() {
       System.out.println("Working...");
   }

   public static void main(String[] args) {
       SetDaemonAfterStartExample afterStartExample = new SetDaemonAfterStartExample();
       afterStartExample.start();
      
       // An exception will be thrown here
       afterStartExample.setDaemon(true);
   }
}
控制台輸出:
正在工作... SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14) 處的 java.lang.Thread.setDaemon(Thread.java:1359) 線程“main”java.lang.IllegalThreadStateException 中的異常

32. 什麼是關閉鉤子?

關閉掛鉤是在 Java 虛擬機 (JVM) 關閉之前隱式調用的線程。因此,我們可以用它來在Java虛擬機正常或異常關閉時釋放資源或保存狀態。我們可以使用以下方法 添加關閉掛鉤:

Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
如示例所示:

/**
* A program that shows how to start a shutdown hook thread,
* which will be executed right before the JVM shuts down
*/
class ShutdownHookThreadExample extends Thread {

   public void run() {
       System.out.println("shutdown hook executed");
   }

   public static void main(String[] args) {

       Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());

       System.out.println("Now the program is going to fall asleep. Press Ctrl+C to terminate it.");
       try {
           Thread.sleep(60000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}
控制台輸出:
現在程序要睡著了。按 Ctrl+C 終止它。關閉掛鉤執行

33.什麼是同步?

在 Java 中,同步是控制多個線程訪問任何共享資源的能力。當多個線程嘗試同時執行同一任務時,您可能會得到不正確的結果。為了解決這個問題,Java 使用了同步,它一次只允許一個線程運行。可以通過三種方式實現同步:
  • 同步一個方法
  • 同步特定塊
  • 靜態同步

同步一個方法

同步方法用於鎖定任何共享資源的對象。當線程調用同步方法時,它會自動獲取對象的鎖,並在線程完成其任務時釋放它。為了使它工作,您需要添加同步關鍵字。我們可以通過看一個例子來了解它是如何工作的:

/**
* An example where we synchronize a method. That is, we add the synchronized keyword to it.
* There are two authors who want to use one printer. Each of them has composed their own poems
* And of course they don’t want their poems mixed up. Instead, they want work to be performed in * * * order for each of them
*/
class Printer {

   synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {
       // One object for two threads
       Printer printer  = new Printer();

       // Create two threads
       Writer1 writer1 = new Writer1(printer);
       Writer2 writer2 = new Writer2(printer);

       // Start them
       writer1.start();
       writer2.start();
   }
}

/**
* Author No. 1, who writes an original poem.
*/
class Writer1 extends Thread {
   Printer printer;

   Writer1(Printer printer) {
       this.printer = printer;
   }

   public void run() {
       List<string> poem = Arrays.asList("I ", this.getName(), " Write", " A Letter");
       printer.print(poem);
   }

}

/**
* Author No. 2, who writes an original poem.
*/
class Writer2 extends Thread {
   Printer printer;

   Writer2(Printer printer) {
       this.printer = printer;
   }

   public void run() {
       List<String> poem = Arrays.asList("I Do Not ", this.getName(), " Not Write", " No Letter");
       printer.print(poem);
   }
}
控制台輸出是這樣的:
我 Thread-0 寫信 我不 Thread-1 不寫信

同步塊

同步塊可用於在方法中對任何特定資源執行同步。假設在一個大方法中(是的,你不應該寫它們,但有時它們會發生)出於某種原因你只需要同步一小部分。如果將方法的所有代碼都放在同步塊中,它將與同步方法一樣工作。語法如下所示:

synchronized ("object to be locked") {
   // The code that must be protected
}
為了避免重複前面的示例,我們將使用匿名類創建線程,即我們將立即實現 Runnable 接口。

/**
* This is how a synchronization block is added.
* Inside the block, you need to specify which object's mutex will be acquired.
*/
class Printer {

   void print(List<String> wordsToPrint) {
       synchronized (this) {
           wordsToPrint.forEach(System.out::print);
       }
       System.out.println();
   }

   public static void main(String args[]) {
       // One object for two threads
       Printer printer = new Printer();

       // Create two threads
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I ", "Writer1", " Write", " A Letter");
               printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I Do Not ", "Writer2", " Not Write", " No Letter");
               printer.print(poem);
           }
       });

       // Start them
       writer1.start();
       writer2.start();
   }
}

}
控制台輸出是這樣的:
我 Writer1 寫一封信 我不寫 Writer2 不寫 沒有信

靜態同步

如果你使靜態方法同步,那麼鎖定將發生在類上,而不是對像上。在此示例中,我們通過將 synchronized 關鍵字應用於靜態方法來執行靜態同步:

/**
* This is how a synchronization block is added.
* Inside the block, you need to specify which object's mutex will be acquired.
*/
class Printer {

   static synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {

       // Create two threads
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I ", "Writer1", " Write", " A Letter");
               Printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I Do Not ", "Writer2", " Not Write", " No Letter");
               Printer.print(poem);
           }
       });

       // Start them
       writer1.start();
       writer2.start();
   }
}
控制台輸出是這樣的:
我不寫信 2 不寫信 我寫信 1 寫信

34.什麼是volatile變量?

在多線程編程中,volatile關鍵字用於線程安全。修改可變變量時,所有其他線程都可以看到更改,因此一個變量一次只能由一個線程使用。通過使用volatile關鍵字,您可以保證變量是線程安全的並存儲在共享內存中,並且線程不會將它存儲在它們的緩存中。這看起來像什麼?

private volatile AtomicInteger count;
我們只是將volatile添加到變量中。但是請記住,這並不意味著完全的線程安全……畢竟,對變量的操作可能不是原子的。也就是說,您可以使用以原子方式執行操作的原子類,即在單個 CPU 指令中。java.util.concurrent.atomic包中有很多這樣的類。

35.什麼是死鎖?

在 Java 中,死鎖是多線程中可能發生的事情。當一個線程正在等待另一個線程獲取的對象鎖,而第二個線程正在等待第一個線程獲取的對象鎖時,就會發生死鎖。這意味著兩個線程正在等待對方,它們的代碼無法繼續執行。Java Core 的前 50 個工作面試問題和答案。 第 2 - 4 部分讓我們考慮一個例子,它有一個實現 Runnable 的類。它的構造函數需要兩個資源。run() 方法按順序為它們獲取鎖。如果你創建了這個類的兩個對象,並以不同的順序傳遞資源,那麼你很容易陷入死鎖:

class DeadLock {

   public static void main(String[] args) {
       final Integer r1 = 10;
       final Integer r2 = 15;

       DeadlockThread threadR1R2 = new DeadlockThread(r1, r2);
       DeadlockThread threadR2R1 = new DeadlockThread(r2, r1);

       new Thread(threadR1R2).start();
       new Thread(threadR2R1).start();
   }
}

/**
* A class that accepts two resources.
*/
class DeadlockThread implements Runnable {

   private final Integer r1;
   private final Integer r2;

   public DeadlockThread(Integer r1, Integer r2) {
       this.r1 = r1;
       this.r2 = r2;
   }

   @Override
   public void run() {
       synchronized (r1) {
           System.out.println(Thread.currentThread().getName() + " acquired resource: " + r1);

           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           synchronized (r2) {
               System.out.println(Thread.currentThread().getName() + " acquired resource: " + r2);
           }
       }
   }
}
控制台輸出:
第一個線程獲取第一個資源 第二個線程獲取第二個資源

36.如何避免死鎖?

因為我們知道死鎖是如何發生的,所以我們可以得出一些結論......
  • 在上面的例子中,死鎖的發生是由於我們有嵌套鎖定。也就是說,我們在同步塊中有一個同步塊。為了避免這種情況,您需要創建一個新的更高抽象層,將同步移至更高級別,並消除嵌套鎖定,而不是嵌套。
  • 鎖定的次數越多,出現死鎖的可能性就越大。因此,每次添加synchronized塊時,都需要考慮自己是否真的需要,是否可以避免添加新的。
  • 使用Thread.join()。您也可能在一個線程等待另一個線程時遇到死鎖。為避免此問題,您可以考慮為join()方法設置超時。
  • 如果我們只有一個線程,那麼就不會有死鎖;)

37. 什麼是競爭條件?

如果現實生活中的比賽涉及汽車,那麼多線程中的比賽就涉及線程。但為什麼?:/ 有兩個線程正在運行並且可以訪問同一個對象。他們可能會同時嘗試更新共享對象的狀態。到目前為止一切都很清楚,對吧?線程要么按字面意義並行執行(如果處理器有多個內核),要么按順序執行,處理器分配交錯的時間片。我們無法管理這些流程。這意味著當一個線程從一個對象讀取數據時,我們不能保證它有時間在其他線程這樣做之前更改對象。當我們有這些“檢查和行動”組合時,就會出現這樣的問題。這意味著什麼?假設我們有一個if語句,它的主體改變了 if 條件本身,例如:

int z = 0;

// Check
if (z < 5) {
// Act
   z = z + 5;
}
當 z 仍然為零時,兩個線程可以同時進入這個代碼塊,然後兩個線程都可以更改它的值。結果,我們不會得到預期值 5。相反,我們會得到 10。如何避免這種情況?您需要在檢查和操作之前獲取鎖,然後再釋放鎖。也就是說,您需要讓第一個線程進入if塊,執行所有操作,更改z,然後才給下一個線程執行相同操作的機會。但是下一個線程不會進入if塊,因為z現在是 5:

// Acquire the lock for z
if (z < 5) {
   z = z + 5;
}
// Release z's lock
===================================================

而不是結論

我想對所有讀到最後的人說聲謝謝。路途遙遠,但你堅持了下來!也許並非一切都清楚。這個是正常的。當我剛開始學習 Java 時,我無法理解什麼是靜態變量。但沒什麼大不了的。我睡在上面,閱讀了更多的資料,然後理解就來了。準備面試更像是一個學術問題,而不是一個實際問題。因此,在每次面試之前,您應該回顧並記住那些您可能不經常使用的東西。

和往常一樣,這裡有一些有用的鏈接:

謝謝大家閱讀。待會兒見 :) 我的 GitHub 個人資料Java Core 的前 50 個工作面試問題和答案。 第 2 - 5 部分
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION