CodeGym /Java 博客 /随机的 /Java Core 的前 50 个工作面试问题和答案。第2部分
John Squirrels
第 41 级
San Francisco

Java Core 的前 50 个工作面试问题和答案。第2部分

已在 随机的 群组中发布
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);
   }
}
控制台输出:
正在工作...线程“主”中的异常 java.lang.Thread.setDaemon(Thread.java:1359) 在 SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14) 的 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