JavaRush /Java Blog /Random-TW /前 50 個 Java 核心面試問題和答案。第三部分
Roman Beekeeper
等級 35

前 50 個 Java 核心面試問題和答案。第三部分

在 Random-TW 群組發布
前 50 個 Java 核心面試問題和答案。第 1 部分 前 50 個 Java 核心面試問題和答案。第2部分

多執行緒

37. Java中如何建立新的執行緒(流)?

不管怎樣,創建是透過使用 Thread 類別進行的。但這裡可能有選擇.....
  1. 我們繼承自java.lang.Thread
  2. 我們實作一個接口java.lang.Runnable,其物件接受建構Thread函式類
讓我們來談談他們中的每一個。

我們繼承Thread類別

為了使這項工作正常進行,在我們的類別中我們繼承了java.lang.Thread. 它包含 meth run(),這正是我們所需要的。新線程的所有生命和邏輯都將在這個方法中。這是一種main新執行緒的方法。之後,剩下的就是創建我們類別的物件並執行方法start(),這將創建一個新線程並運行其中編寫的邏輯。我們看看吧:
/**
* Пример того, How создавать треды путем наследования {@link Thread} класса.
*/
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();
   }
}
控制台的輸出將如下所示:

Thread-1
Thread-0
Thread-2
也就是說,即使在這裡我們看到線程也不是按順序執行的,而是按照 JVM 的決定執行的)

實作Runnable介面

如果您反對繼承和/或已經繼承了其他類別之一,則可以使用java.lang.Runnable. 在我們的類別中,我們實作了這個介面並實作了方法run(),就像在那個例子中一樣。您只需要建立更多物件Thread。看來線路越多越糟。但我們知道繼承有多麼有害,最好無論如何避免它;)讓我們看看:
/**
* Пример того, How создавать треды из интерфейса {@link Runnable}.
* Здесь проще простого - реализуем этот интерфейс и потом передаем в конструктор
* экземпляр реализуемого an object.
*/
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();
   }
}
以及執行結果:

Thread-0
Thread-1
Thread-2

38.進程和執行緒有什麼差別?

前 50 個 Java 核心面試問題和答案。 第 3 - 1 部分行程和執行緒之間有以下區別:
  1. 正在執行的程式稱為進程,而執行緒是進程的子集。
  2. 進程是獨立的,而執行緒是進程的子集。
  3. 進程在記憶體中具有不同的位址空間,而執行緒則包含公共位址空間。
  4. 與進程相比,執行緒之間的上下文切換更快。
  5. 進程間通訊比執行緒間通訊更慢且更昂貴。
  6. 父行程中的任何變更不會影響子行程,而父執行緒中的變更會影響子執行緒。

39.多執行緒有哪些優點?

前 50 個 Java 核心面試問題和答案。 第 3 - 2 部分
  1. 多執行緒允許應用程式/程式始終響應輸入,即使它已經在運行一些後台任務;
  2. 多執行緒允許您更快地完成任務,因為執行緒獨立執行;
  3. 多線程提供了更好的快取利用率,因為線程共享公共記憶體資源;
  4. 多執行緒減少了所需的伺服器數量,因為一台伺服器可以同時執行多個執行緒。

40. 執行緒的生命週期有哪些狀態?

前 50 個 Java 核心面試問題和答案。 第 3 - 3 部分
  1. New:在此狀態下,Thread使用new運算子建立了一個類別對象,但線程不存在。直到我們調用start().
  2. Runnable:在此狀態下,呼叫方法後執行緒已準備好執行 開始()。然而,它還沒有被線程調度程序選擇。
  3. 運行:在此狀態下,執行緒調度程序從就緒狀態中選擇一個執行緒並執行。
  4. 等待/阻塞:在此狀態下,執行緒未執行,但仍處於活動狀態或正在等待另一個執行緒完成。
  5. 死亡/終止:當方法退出時,run()執行緒處於終止或死亡狀態。

41. 是否可以啟動一個執行緒兩次?

不,我們不能重新啟動線程,因為一旦線程啟動並執行,它就會進入 Dead 狀態。因此,如果我們嘗試運行該線程兩次,它將拋出一個runtimeException“ java.lang.IllegalThreadStateException ”。我們看看吧:
class DoubleStartThreadExample extends Thread {

   /**
    * Имитируем работу треда
    */
   public void run() {
	// что-то происходит. Для нас не существенно на этом этапе
   }

   /**
    * Запускаем тред дважды
    */
   public static void main(String[] args) {
       DoubleStartThreadExample doubleStartThreadExample = new DoubleStartThreadExample();
       doubleStartThreadExample.start();
       doubleStartThreadExample.start();
   }
}
一旦工作到達同一執行緒的第二次啟動,就會出現異常。親自嘗試;)見一次勝於聽一百次。

42.如果直接呼叫run()方法而不呼叫start()方法會怎麼樣?

是的,run()您當然可以呼叫一個方法,但這不會創建一個新線程並將其作為單獨的線程執行。在本例中,它是一個呼叫簡單方法的簡單物件。如果我們談論的是方法start(),那就是另一回事了。透過啟動此方法,runtime它會啟動一個新方法,然後運行我們的方法;)如果您不相信我,請嘗試:
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();

       // просто будут вызваны в потоке main два метода, один за другим.
       runExample1.run();
       runExample2.run();
   }
}
控制台的輸出將如下所示:

0123401234
可以看到沒有創建任何線程。一切都像正常的課堂一樣進行。首先第一個類別方法起作用了,然後是第二個方法。

43.什麼是守護線程?

前 50 個 Java 核心面試問題和答案。 第 3 - 4 部分守護線程(以下簡稱守護線程)是在後台執行與另一個線程相關的任務的線程。也就是說,它的工作是執行僅需要與另一個(主)執行緒一起完成的輔助任務。有很多自動運行的守護線程,例如垃圾收集器、終結器等。

Java為什麼要關閉守護線程?

守護線程的唯一目的是為使用者執行緒提供服務以完成後台支援任務。因此,如果主線程已完成,則運行時會自動關閉其所有守護線程。

在 Thread 類別中工作的方法

這個類別java.lang.Thread提供了兩種使用線程守護程序的方法:
  1. public void setDaemon(boolean status)- 表明這將是一個守護線程。預設值為false,這表示除非單獨指定,否則將建立非守護線程。
  2. public boolean isDaemon()daemon- 本質上,這是我們使用前面的方法設定的變數的 getter 。
例子:
class DaemonThreadExample extends Thread {

   public void run() {
       // Проверяет, демон ли этот поток or нет
       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();

       // теперь thread1 - поток-демон.
       thread1.setDaemon(true);

       System.out.println("демон?.. " + thread1.isDaemon());
       System.out.println("демон?.. " + thread2.isDaemon());
       System.out.println("демон?.. " + thread3.isDaemon());

       thread1.start();
       thread2.start();
       thread3.start();
   }
}
控制台輸出:

демон?.. true
демон?.. false
демон?.. false
daemon thread
user thread
user thread
從輸出我們看到,在線程本身內部,使用靜態currentThread()方法,我們一方面可以找出它是哪個線程,另一方面,如果我們有這個線程的物件的引用,我們可以找出直接來自它。這為配置提供了必要的靈活性。

44. 線程創建後是否可以使其成為守護程式?

不。如果你這樣做,它會拋出異常IllegalThreadStateException。因此,我們只能在啟動之前創建一個守護線程。例子:
class SetDaemonAfterStartExample extends Thread {

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

   public static void main(String[] args) {
       SetDaemonAfterStartExample afterStartExample = new SetDaemonAfterStartExample();
       afterStartExample.start();

       // здесь будет выброшено исключение
       afterStartExample.setDaemon(true);
   }
}
控制台輸出:

Working...
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.setDaemon(Thread.java:1359)
	at SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)

45.什麼是shutdownhook?

Shutdownhook是一個在 JVM(Java 虛擬機器)關閉之前隱含呼叫的執行緒。因此,當Java虛擬機器正常或突然關閉時,我們可以使用它來清理資源或儲存狀態。我們可以shutdown hook使用以下方法來新增:
Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
如範例所示:
/**
* Программа, которая показывает How запустить shutdown hook тред,
* который выполнится аккурат до окончания работы JVM
*/
class ShutdownHookThreadExample extends Thread {

   public void run() {
       System.out.println("shutdown hook задачу выполнил");
   }

   public static void main(String[] args) {

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

       System.out.println("Теперь программа засыпает, нажмите ctrl+c чтоб завершить ее.");
       try {
           Thread.sleep(60000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}
控制台輸出:

Теперь программа засыпает, нажмите ctrl+c чтоб завершить ее.
shutdown hook задачу выполнил

46.什麼是同步?

Java中的同步是控制多個執行緒對任何共享資源的存取的能力。當多個執行緒嘗試執行相同任務時,可能會出現錯誤結果,因此為了克服這個問題,Java 使用同步,因為同步只能有一個執行緒在同一時間工作。同步可以透過三種方式實現:
  • 同步方式
  • 透過同步特定區塊
  • 靜態同步

方法同步

同步方法用於鎖定任何共享資源的物件。當執行緒呼叫同步方法時,它會自動取得該物件的鎖,並在執行緒完成其任務時釋放它。為了讓它工作,你需要添加synchronized關鍵字。讓我們透過一個例子來看看它是如何運作的:
/**
* Пример, где мы синхронизируем метод. То есть добавляем ему слово synchronized.
* Есть два писателя, которые хотят использовать один принтер. Они подготовor свои поэмы
* И конечно же не хотят, чтоб их поэмы перемешались, а хотят, чтоб работа была сделана по * * * очереди для каждого из них
*/
class Printer {

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

   public static void main(String args[]) {
       // один an object для двух тредов
       Printer printer  = new Printer();

       // создаем два треда
       Writer1 writer1 = new Writer1(printer);
       Writer2 writer2 = new Writer2(printer);

       // запускаем их
       writer1.start();
       writer2.start();
   }
}

/**
* Писатель номер 1, который пишет свою поэму.
*/
class Writer1 extends Thread {
   Printer printer;

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

   public void run() {
       List<string> poem = Arrays.asList("Я ", this.getName(), " Пишу", " Письмо");
       printer.print(poem);
   }

}

/**
* Писатель номер 2, который пишет свою поэму.
*/
class Writer2 extends Thread {
   Printer printer;

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

   public void run() {
       List<String> poem = Arrays.asList("Не Я ", this.getName(), " Не пишу", " Не Письмо");
       printer.print(poem);
   }
}
以及控制台的輸出:

Я Thread-0 Пишу Письмо
Не Я Thread-1 Не пишу Не Письмо

同步區塊

同步區塊可用於對任何特定方法資源執行同步。假設在一個大型方法中(是的,是的,你不能寫這樣的東西,但有時會發生),出於某種原因,你只需要同步一小部分。如果將一個方法的所有程式碼放在一個同步區塊中,它的工作方式與同步方法相同。文法如下:
synchronized (“an object для блокировки”) {
   // сам code, который нужно защитить
}
為了不重複前面的例子,我們將透過匿名類別建立執行緒-也就是立即實作Runnable介面。
/**
* Вот How добавляется блок синхронизации.
* Внутри нужно указать у кого будет взят мьютекс для блокировки.
*/
class Printer {

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

   public static void main(String args[]) {
       // один an object для двух тредов
       Printer printer = new Printer();

       // создаем два треда
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("Я ", "Writer1", " Пишу", " Письмо");
               printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("Не Я ", "Writer2", " Не пишу", " Не Письмо");
               printer.print(poem);
           }
       });

       // запускаем их
       writer1.start();
       writer2.start();
   }
}

}
並輸出到控制台

Я Writer1 Пишу Письмо
Не Я Writer2 Не пишу Не Письмо

靜態同步

如果將靜態方法設定為同步,則鎖將位於類別上,而不是物件上。在本例中,我們對靜態方法套用synchronized關鍵字來進行靜態同步:
/**
* Вот How добавляется блок синхронизации.
* Внутри нужно указать у кого будет взят мьютекс для блокировки.
*/
class Printer {

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

   public static void main(String args[]) {

       // создаем два треда
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("Я ", "Writer1", " Пишу", " Письмо");
               Printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("Не Я ", "Writer2", " Не пишу", " Не Письмо");
               Printer.print(poem);
           }
       });

       // запускаем их
       writer1.start();
       writer2.start();
   }
}
以及控制台的輸出:

Не Я Writer2 Не пишу Не Письмо
Я Writer1 Пишу Письмо

47.什麼是 volatile 變數?

該關鍵字volatile在多線程編程中用於提供線程安全性,因為對一個可變變量的修改對所有其他線程都是可見的,因此一個變數一次只能由一個線程使用。使用該關鍵字,volatile可以保證變數是線程安全的,並將儲存在共享記憶體中,線程不會將其放入快取中。它是什麼樣子的?
private volatile AtomicInteger count;
我們只需添加到變數中即可volatile。但這並不意味著完全的線程安全......畢竟,對變數的操作可能不是原子的。但是您可以使用Atomic以原子方式(即由處理器一次執行)執行操作的類別。包中可以找到許多這樣的類別java.util.concurrent.atomic

48.什麼是死鎖

Java 中的死鎖是多執行緒的一部分。當一個執行緒正在等待另一個執行緒取得的物件鎖,而第二個執行緒正在等待第一個執行緒取得的物件鎖時,可能會發生死鎖。因此,這兩個執行緒互相等待,並且不會繼續執行它們的程式碼。 前 50 個 Java 核心面試問題和答案。 第 3 - 5 部分讓我們來看一個範例,其中有一個實作 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();
   }
}

/**
* Класс, который принимает два ресурса.
*/
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() + " захватил ресурс: " + r1);

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

           synchronized (r2) {
               System.out.println(Thread.currentThread().getName() + " захватил ресурс: " + r2);
           }
       }
   }
}
控制台輸出:

Первый тред захватил первый ресурс
Второй тред захватывает второй ресурс

49. 如何避免死鎖?

根據我們所知道的死鎖是如何發生的,我們可以得出一些結論...
  • 如上例所示,死鎖是由於鎖的嵌套造成的。也就是說,一把鎖內有另一把或更多把鎖。可以透過以下方式避免這種情況 - 您需要在頂部添加新的抽象並將鎖賦予更高級別,而不是嵌套,並刪除嵌套鎖。
  • 阻塞越多,發生死鎖的可能性就越大。因此,每次加一把鎖的時候,都需要思考是否真的需要,是否可以避免加一把新的鎖。
  • 用途Thread.join(). 當一個執行緒等待另一個執行緒時,也可能會發生死鎖。為了避免此問題,您可以考慮對該方法設定時間限制join()
  • 如果我們有一個線程,就不會有死鎖;)

50.什麼是競爭條件?

如果汽車在真實的比賽中表現出色,那麼在多線程的賽車術語中,線程在比賽中表現。但為什麼?有兩個執行緒正在運行並且可以存取同一物件。他們可以嘗試同時更新狀態。到目前為止一切都清楚了,對吧?因此,當處理器分配一小段時間時,執行緒要么真正並行工作(如果處理器中有多個核心),要么有條件並行工作。而且我們無法控制這些過程,因此我們無法保證當一個執行緒從物件讀取資料時,它有時間在其他執行緒執行此操作之前更改它。當這種「測試與行動」組合進行時,就會出現這樣的問題。這是什麼意思?例如,我們if在主體中有一個表達式,其條件本身會發生變化,即:
int z = 0;

// проверь
if (z < 5) {
//действуй
   z = z + 5;
}
因此可能存在這樣的情況:兩個執行緒同時進入該程式碼區塊,同時 z 仍等於 0,並且它們一起更改該值。而最終我們得到的不是期望值5,而是10。如何避免這種情況?執行前後都需要加鎖。也就是說,第一個執行緒進入區塊if,執行所有操作,更改它,z然後才給下一個執行緒執行此操作的機會。但下一個執行緒不會進入 block if,因為z它已經等於 5:
// получить блокировку для z
if (z < 5) {
   z = z + 5;
}
// выпустить из блокировки z
===================================================

而不是輸出

我想對所有讀到最後的人表示感謝。這是一段漫長的旅程,而你成功了!並非一切都清楚。這可以。當我開始學習 Java 時,我無法理解靜態變數是什麼。但沒什麼,我帶著這個想法入睡,又讀了一些資料,終於明白了。準備面試更多的是一個學術問題,而不是一個實際問題。因此,在每次面試之前,你需要重複並刷新你可能不經常使用的東西的記憶。

一如既往,有用的連結:

感謝大家閱讀,再見) 我在 GitHub 上的個人資料
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION