2025 届秋招常见八股文汇总——Java 多线程

Java 创建线程有哪几种方式

在 Java 中,创建线程有四种方式,分别是继承 Thread 类、实现 Runnable 接口,使用 Callable 和 Future,使用线程池.

  1. 继承 Thread 类: 通过创建 Thread 类的子类,并重写其 run 方法来定义线程执行的任务。
  2. 实现 Runnable 接口: 创建一个实现了 Runnable 接口的类,并实现其 run 方法。然后创建该类的实例,并将其作为参数传递给 Thread 对象。
  3. 使用 Callable 和 Future 接口:创建一个实现了 Callable 接口的类,并实现其 call 方法,该方法可以返回结果并抛出异常。使用 ExecutorService 来管理线程池,并提交 Callable 任务获取 Future 对象,以便在未来某个时刻获取 Callable 任务的计算结果。
  4. 使用线程池:通过使用 ThreadPoolExecutor 类创建线程池,并通过线程池来管理线程的创建和复用。

Thread.start() 和 Thread.run() 的区别

在 Java 多线程中,run 方法和 start 方法的区别在于:

  1. run 方法是线程的执行体,包含线程要执行的代码,当直接调用 run 方法时,它会在当前线程的上下文中执行,而不会创建新的线程。
  2. start 方法用于启动一个新的线程,并在新线程中执行 run 方法的代码。调用 start 方法会为线程分配系统资源,并将线程置于就绪状态,当调度器选择该线程时,会执行 run 方法中的代码。

因此,虽然可以直接调用 run 方法,但这并不会创建一个新的线程,而是在当前线程中执行 run 方法的代码。如果需要实现多线程执行,则应该调用 start 方法来启动新线程。

Java 线程的 6 个状态

  1. NEW:新建状态,线程被创建但未启动。
  2. RUNNABLE:可运行状态,线程已启动并在等待 CPU 调度。
  3. BLOCKED:阻塞状态,线程等待获取锁。
  4. WAITING:等待状态,线程等待其他线程的通知。
  5. TIMED_WAITING:计时等待状态,线程等待一段时间后自动苏醒。
  6. TERMINATED:终止状态,线程已完成执行。

Java 中有哪些锁

  1. 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优 先获取锁。有可能,会造成优先级反转或者饥饿现象。 对于 Java ReentrantLock 而言,默认是非公平锁,对于 Synchronized 而言,也是一种非公平锁。
  2. 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。对于 Java ReentrantLock 而言,是可重入锁,对于 Synchronized 而言,也是一个可重入锁。
  3. 独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。对于 Java ReentrantLock 而言,其是独享锁。但是对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。 对于 Synchronized 而言,当然是独享锁。
  4. 互斥锁/读写锁:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在 Java 中的具体实现就是 ReentrantLock。读写锁在 Java 中的具体实现就是 ReadWriteLock。
  5. 乐观锁/悲观锁:乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。 悲观锁在 Java 中的使用,就是利用各种锁。乐观锁在 Java 中的使用,是无锁编程,常常采用的是 CAS 算法,典型的例子就是原子类,通过 CAS 自旋实现原子操作的更新。
  6. 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
  7. 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对 Synchronized。在 Java 5 通过引入锁升级的机制来实现高效 Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  8. 自旋锁:在 Java 中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。

如何理解 synchronized 关键字

synchronized 用于实现同步和线程安全。

  • 当一个方法或代码块被 synchronized 修饰时,它将成为一个临界区,同一时刻只能由一个线程访问。其他线程必须等待当前线程退出临界区才能进入。确保多个线程在访问共享资源时不会产生冲突。
  • synchronized 可以应用于方法或代码块。当它应用于方法时,整个方法被锁定;当它应用于代码块时,只有该代码块被锁定。这样做的好处是,可以选择性地锁定对象的一部分,而不是整个方法。
  • synchronized 实现的机理依赖于软件层面上的 JVM,因此其性能会随着 Java 版本的不断升级而提高。 到了 Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。 需要说明的是,当线程通过 synchronized 等待锁时是不能被 Thread.interrupt() 中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。
  • 最后,尽管 Java 实现的锁机制有很多种,并且有些锁机制性能也比 synchronized 高,但还是强烈推荐在多线程应用程序中使用该关键字,因为实现方便,后续工作由 JVM 来完成,可靠性高。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如 ReentrantLock 等。

synchronized 和 lock 的区别是什么

synchronized 和 Lock 都是 Java 中用于实现线程同步的手段,synchronized 是 Java 的关键字,基于 JVM 的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而 Lock 是一个接口,是 Java 提供的显式锁机制,需要手动获取和释放锁,通过实现类(如 ReentrantLock)来创建锁对象,然后主动调用锁的获取和释放方法。

特性

  • synchronized:灵活性相对较低,只能用于方法或代码块。而且 synchronized 方法一旦开始执行,即使线程被阻塞,也不能中断。没有超时机制,一旦获取不到锁就会一直等待,也没有公平性的概念,线程调度由 JVM 控制。
  • lock:提供了更多的灵活性,例如可以尝试获取锁,如果锁已被其他线程持有,可以选择等待或者中断等待。提供了超时获取锁的能力,可以在指定时间内尝试获取锁,也可以设置为公平锁,按照请求锁的顺序来获取锁。

等待与通知:

  • synchronized:与 wait() 和 notify()/notifyAll() 方法一起使用,用于线程的等待和通知。
  • lock:可以与 Condition 接口结合,实现更细粒度的线程等待和通知机制。

总结来说,synchronized 使用简单,适合锁的粒度较小、竞争不激烈、实现简单的场景。而 Lock 提供了更多的灵活性和控制能力,适用于需要更复杂同步控制的场景。

synchronized 和 ReentrantLock 的区别是什么

synchronized 和 ReentrantLock 都是 Java 中用于实现线程同步的手段,synchronized 是 Java 的关键字,基于 JVM 的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而 ReentrantLock 是 java.util.concurrent.locks 包中的一个锁实现,需要显式创建,并通过调用 lock() 和 unlock() 方法来管理锁的获取和释放。

特性

  • synchronized:灵活性相对较低,只能用于方法或代码块。而且synchronized方法一旦开始执行,即使线程被阻塞,也不能中断。没有超时机制,一旦获取不到锁就会一直等待,也没有公平性的概念,线程调度由JVM控制。
  • ReentrantLock:支持中断操作,可以在等待锁的过程中响应中断, 提供了尝试获取锁的超时机制,可以通过tryLock()方法设置超时时间。可以设置为公平锁,按照请求的顺序来获取锁,提供了isLocked()、isFair()等方法,可以检查锁的状态。

条件变量

  • synchronized 可以通过 wait()、notify()、notifyAll() 与对象的监视器方法配合使用来实现条件变量。
  • ReentrantLock 可以通过 Condition 新 API 实现更灵活的条件变量控制。

锁绑定多个条件

  • synchronized 与单个条件关联,需要使用多个方法调用来实现复杂的条件判断。
  • ReentrantLock 可以与多个 Condition 对象关联,每个对象可以有不同的等待和唤醒逻辑。

总结来说,synchronized 适合简单的同步需求,而 ReentrantLock 提供了更丰富的控制能力和灵活性,适用于需要复杂同步控制的场景。

volatile 关键字的作用有哪些

volatile 通常被比喻成 “轻量级的 synchronized”,它不需要获取和释放锁,是 Java 并发编程中比较重要的一个关键字。 和 synchronized 不同,volatile 是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。

volatile 关键字在 Java 中主要用于保证变量的内存可见性和禁止指令重排。

保证可见性: 确保当一个线程修改了一个 volatile 变量时,其他线程能够立即看到这个改变。

  • 当对非 volatile 变量进行读写的时候,每个线程先从主内存拷贝变量到 CPU 缓存中,如果计算机有多个 CPU,每个线程可能在不同的 CPU 上 被处理,这意味着每个线程可以拷贝到不同的 CPU cache中。
  • volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过 CPU cache 这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。

禁止指令重排: volatile 变量的写操作在 JVM 执行时不会发生指令重排,确保写入操作在读取操作之前完成。

  • 指令重排序是 JVM 为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度, 包括编译器重排序和运行时重排序;
  • volatile 变量禁止指令重排序。针对 volatile 修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏障。

虽然 volatile 可以确保可见性,但它不保证复合操作的原子性。

volatile 与 synchronized 的对比

volatile 和 synchronized 都是 Java 中用于多线程同步的工具,在用途、原子性、互斥性、性能和使用场景上有一定的区别。

  1. 机制和用途
  • synchronized:用于提供线程间的同步机制。当一个线程进入一个由 synchronized 修饰的代码块或方法时,它会获取一个监视器锁,这保证了同一时间只有一个线程可以执行这段代码。其主要用途是确保数据的一致性和线程安全性。
  • volatile:用于修饰变量。volatile 的主要作用是确保变量的可见性,即当一个线程修改了一个 volatile 变量的值,其他线程能够立即看到这个修改。此外,它还可以防止指令重排序。但是,volatile 并不能保证复合操作的原子性。
  • 总结: volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
  1. 原子性
  • synchronized:可以保证被其修饰的代码块的原子性,即这段代码在执行过程中不会被其他线程打断。
  • volatile:只能保证单个读写操作的原子性,对于复合操作(如自增、自减等)则无法保证原子性。
  1. 性能

volatile 通常比 synchronized 更轻量级,所以 volatile 性能肯定比 synchronized 关键字要好; 因为它不涉及锁的获取和释放。但是,这也意味着它提供的同步级别较低。

  1. 互斥性:
  • synchronized:提供了互斥性,即同一时间只有一个线程可以执行被其修饰的代码块或方法。
  • volatile:不提供互斥性,只是确保变量的可见性。
  1. 使用场景
  • volatile 适用于简单的内存可见性要求
  • 而 synchronized 可以通过 ReentrantLock 等扩展为更灵活的锁机制,适用于需要保证原子性、可见性和互斥的复杂同步场景。

线程池的作用

  • 资源管理: 在多线程应用中,每个线程都需要占用内存和 CPU 资源,如果不加限制地创建线程,会导致系统资源耗尽,可能引发系统崩溃。线程池通过限制并控制线程的数量,帮助避免这个问题。
  • 提高性能:通过重用已存在的线程,线程池可以减少创建和销毁线程的开销。
  • 任务排队:线程池通过任务队列和工作线程的配合,合理分配任务,确保任务按照一定的顺序执行,避免线程竞争和冲突
  • 统一管理:线程池提供了统一的线程管理方式,可以对线程进行监控、调度和管理。

线程池有哪些常用参数

  • corePoolSize 核心线程数:线程池中长期存活的线程数。
  • maximumPoolSize 最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
  • keepAliveTime 空闲线程存活时间:当线程数大于 corePoolSize 时,多余的空闲线程能等待新任务的最长时间。
  • TimeUnit:与 keepAliveTime 一起使用,指定 keepAliveTime 的时间单位,如秒、分钟等。
  • workQueue 线程池任务队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。
  • ThreadFactory:线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
  • RejectedExecutionHandler:拒绝策略,当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。

BIO、NIO、AIO 的区别

BIO、AIO 和 NIO 是 Java 中不同的 I/O 模型,它们在处理输入输出操作时有不同的特点。

  • BIO:阻塞式的 I/O 模型。当一个线程执行 I/O 操作时,如果数据还没准备好,这个线程会被阻塞,直到数据到达。适合连接数较少且固定的场景,但扩展性较差。
  • NIO:非阻塞的 I/O 模型。NIO 使用缓冲区和通道来处理数据,提高了 I/O 操作的效率。支持面向缓冲区的读写操作,可以处理大量并发的连接。
  • AIO:异步I/O模型,从 Java 7 开始引入。在 AIO 中,I/O 操作被发起后,线程可以继续执行其他任务,一旦 I/O 操作完成,操作系统会通知线程。适合需要处理大量并发 I/O 操作,且希望避免 I/O 操作阻塞线程的场景。
  • 使用场景:
    • BIO 适合低并发、连接数较少的应用。
    • NIO 适合高并发、需要处理大量连接的应用。
    • AIO 适合需要高性能、异步处理 I/O 操作的场景。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注