📜  前 20 个Java多线程面试问题和答案(1)

📅  最后修改于: 2023-12-03 15:07:17.116000             🧑  作者: Mango

前20个Java多线程面试问题和答案

1. 简述Java线程的状态以及状态之间如何转换?

Java线程状态包括:新建(New)、就绪(Runnable)、阻塞(Blocked)、运行(Running)和死亡(Dead)五种状态。状态之间的转换如下:

  • 新建 -> 就绪:线程已被创建但未调用start()方法。
  • 就绪 -> 运行:调用了start()方法。
  • 运行 -> 阻塞:线程等待某个事件的发生,如等待IO操作完成、等待锁等。
  • 阻塞 -> 就绪:等待的事件已经发生。
  • 运行 -> 就绪:线程被其他线程打断,例如调用了Thread.yield()方法。
  • 就绪 -> Dead:run()方法执行结束,线程死亡。
2. 什么是线程安全?如何实现线程安全?

线程安全是指在多线程环境下,程序仍然能够正确地执行。实现线程安全的方法有:

  • 使用线程安全的数据结构,例如ConcurrentHashMapCopyOnWriteArrayList等。
  • 使用synchronized关键字对方法或代码块进行加锁控制。
  • 使用Lock接口及其实现类控制代码块的并发访问。
  • 使用volatile关键字保证变量的可见性,避免出现脏读、重排序等问题。
3. synchronized关键字和Lock接口的区别?

synchronized是Java关键字,可以用于修饰方法或代码块实现同步。它的实现依赖于Java虚拟机,性能比较好,但灵活性较差。

Lock接口是Java5中新增的接口,通过Lock可以实现更灵活、更精确的线程同步。它比synchronized更加灵活,支持公平锁、非公平锁、可重入锁等特性,但是由于Lock在使用时需要手动获取锁和释放锁,使用不当容易导致死锁等问题。

4. Thread.sleep()方法和wait()方法有什么区别?

Thread.sleep()方法和wait()方法的区别如下:

  • Thread.sleep()是Thread类的静态方法,让当前线程睡眠一段时间,不会释放锁。wait()是Object类的方法,让线程等待并释放锁。
  • 调用sleep()方法不需要获取锁。调用wait()方法前必须获取锁。
  • 调用wait()方法后,线程将阻塞等待,直到其他线程调用notify()notifyAll()方法唤醒该线程。调用sleep()方法后,线程将一直睡眠,直到经过指定的时间或其他线程中断该线程。
5. 什么是线程池?如何创建线程池?

线程池是一种线程复用的机制,通过预先创建一些线程并维护一个线程队列来提高线程的使用效率和响应时间。

创建线程池的方法如下:

ExecutorService threadPool = Executors.newFixedThreadPool(10);

这里创建了一个固定大小的线程池,大小为10。Executors是一个工具类,提供了许多静态方法,可以用来创建不同类型的线程池。

6. 线程池的好处是什么?

线程池的好处如下:

  • 提高性能:线程池可以避免线程的创建和销毁造成的性能消耗,可以提高系统的响应速度。
  • 提高可维护性:可以统一管理线程池中的线程,可以统一设置线程的属性、监控线程的运行状态等,易于维护。
  • 提高扩展性:可以通过修改线程池的配置参数快速适应不同的业务场景,提高系统的扩展能力。
7. Java中的线程是否是抢占式调度?

Java中的线程是抢占式调度的。当有多个线程在竞争CPU资源时,Java虚拟机会根据线程的优先级和其他状态对线程进行抢占式调度。

8. 说一下Java内存模型(JMM)?

Java内存模型是Java程序中所有线程共享的内存区域的抽象。在JMM中,所有变量都存储在主内存中,每个线程还有自己独立的工作内存,工作内存保存了主内存中变量的副本。

在JMM中,线程之间的通信通过主内存进行,在进行读写操作时会从主内存中读取变量的值或将变量的值写回到主内存中。由于JMM的存在,可能会出现变量读写顺序的重排序、数据竞争等问题,影响程序的正确性和性能。

9. 什么是守护线程?如何创建守护线程?

守护线程是一种特殊的线程,它会在所有非守护线程结束后自动退出。守护线程在Java中的应用比较广泛,例如JVM的垃圾回收线程就是守护线程。

创建守护线程的方法如下:

Thread thread = new Thread(runnable);
thread.setDaemon(true);
thread.start();

这里使用setDaemon(true)方法设置线程为守护线程。注意,必须在调用start()方法之前调用setDaemon()方法。

10. 什么是ThreadLocal?

ThreadLocal是Java中的一个线程局部变量,每个线程都有自己的ThreadLocal变量副本。不同线程之间互不干扰,可以保证线程安全。

ThreadLocal常用于保存线程上下文信息、数据库连接、记录当前用户等场景。

11. 什么是同步锁?

同步锁是Java中用于实现多线程同步的机制,通过对共享资源加锁来保证只有一个线程可以访问该资源。Java中提供了两种实现同步锁的机制:synchronized和Lock。

12. synchronized关键字如何实现线程同步?

synchronized通过对方法或代码块块进行加锁来实现线程同步。它具有以下特性:

  • 可重入性:一个线程已经持有锁,可以继续对同步代码块进行加锁。
  • 可见性:一个线程获取锁之后,可以保证后续所有操作都能看到之前操作的结果。
  • 原子性:一个线程执行同步代码块期间,其他线程无法访问该代码块持有的资源。
13. Lock接口如何实现线程同步?

实现同步锁的另一种方式是使用Lock接口及其实现类。Lock具有更灵活、更加精确的特性,例如支持公平锁、非公平锁、可重入锁等。

使用Lock接口实现线程同步的代码如下:

Lock lock = new ReentrantLock();
lock.lock();
try {
  // 具体业务逻辑
} finally {
  lock.unlock();
}
14. 线程之间如何通信?

线程之间可以通过共享变量、管道、信号量、自旋锁等方式进行通信。其中最重要的是共享变量的方式,Java中的synchronizedvolatile关键字都是基于共享变量实现线程之间的通信的。

15. 什么是线程死锁?

线程死锁是指两个或多个线程互相持有对方需要的资源而无法继续执行的状态。线程死锁通常是程序设计或资源分配问题造成的,一旦出现会导致系统停止响应,严重影响程序的正确性和性能。

16. 什么是线程饥饿?

线程饥饿是指某个线程无法获得所需的资源而无法继续执行的状态。通常是由于低优先级线程得不到资源,而高优先级线程占用了资源造成的。

17. 什么是线程优先级?如何设置线程优先级?

线程优先级是指线程在竞争CPU资源时的优先级,有10个级别,范围从1到10,其中10为最高优先级。可以通过setPriority()方法设置线程的优先级,如下:

Thread thread = new Thread(runnable);
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();

这里设置了线程的优先级为最大优先级。

18. 什么是CountDownLatch?

CountDownLatch是Java中的一个同步工具,可以用于控制多个线程之间的执行顺序,特别是一个线程A需要等待多个其他线程B、C、D等处理完之后才能执行。

使用CountDownLatch的方法如下:

CountDownLatch latch = new CountDownLatch(3); // 创建一个计数器,初始值为3
// 在多个线程中调用latch.countDown()方法,减少计数器的值
latch.await(); // 等待所有线程执行完毕

这里同时创建了三个线程执行某个业务逻辑,这三个线程都会在执行完毕后调用latch.countDown()方法,最后主线程再调用latch.await()方法等待这三个线程完成。

19. 什么是闭锁?

闭锁也是一种同步工具,与CountDownLatch类似,用于暂停线程的执行,等待满足条件后再继续执行。不同之处在于,闭锁可以重复使用,而CountDownLatch只能使用一次。

Java中提供了一个java.util.concurrent.CyclicBarrier类实现闭锁的功能。

20. 什么是线程池的拒绝策略?

线程池的拒绝策略是指当线程池的任务队列已满时,预定义的线程池可以采取的策略。JDK中提供了以下四种预定义的拒绝策略:

  • AbortPolicy:抛出RejectedExecutionException异常,默认策略。
  • CallerRunsPolicy:由调用线程(提交任务的线程)执行该任务。
  • DiscardOldestPolicy:丢弃任务队列中最旧的一个任务,执行当前的任务。
  • DiscardPolicy:丢弃当前的任务。

可以通过调用ThreadPoolExecutor的构造方法来指定线程池使用的拒绝策略。