JavaRush /Java 博客 /Random-ZH /前 50 个 Java 核心面试问题和答案。第三部分
Roman Beekeeper
第 35 级

前 50 个 Java 核心面试问题和答案。第三部分

已在 Random-ZH 群组中发布
前 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