前言

本文需要有一定的java基础才能更好观看,背诵记忆的话只需继续略微精简即可,如果有疑问或者需要案例可以观看B站诸葛老师视频Java基础面试题100问大合集,小白面试学习(全套通俗易懂)_哔哩哔哩_bilibili。如果还有疑问或者想要讨论的话可以评论区留言。将会持续更新。

一、sleep()、wait()、join()、yield() 的区别

在多线程编程中,理解这几个方法的区别至关重要。它们都涉及线程的暂停或等待,但使用场景和行为完全不同。

  • 锁池:当某线程尝试进入一个已被其他线程持有的 synchronized 代码块时,该线程会进入这个锁对象的锁池。线程在此等待锁被释放,并参与后续的锁竞争。

  • 等待池:当已经获得对象锁的线程在同步代码块内部调用该对象的 wait() 方法时,它会释放锁并进入这个对象的等待池。线程在此等待被其他线程通过 notify() 或 notifyAll() 方法唤醒。

sleep() 与 wait() 核心区别对比

特性 Thread.sleep() Object.wait()
所属类 Thread 类的静态方法 Object 类的实例方法
调用前提 任何情况下都可调用 必须在同步代码块或同步方法内(即必须先获得对象锁)
锁的行为 不释放已持有的任何锁 立即释放已持有的对象锁
主要目的 让当前线程暂停执行指定的时间 让当前线程等待,并允许其他线程进入同步区域
唤醒机制 超时后自动唤醒或被中断(InterruptedException 需被其他线程用 notify()/notifyAll() 唤醒或被中断
用途 模拟延迟、执行定时任务 实现线程间协作(如生产者-消费者模型)
  • Thread.sleep():使当前线程暂停执行指定的毫秒数。它只关心时间,不关心锁或其他线程的状态。最关键的是,在睡眠期间,它不会释放已经持有的任何锁,这可能会导致其他需要相同锁的线程被阻塞。

  • Object.wait():用于线程间通信。它让当前线程进入等待状态,并立即释放持有的对象锁,从而使其他线程能够获得该锁并执行同步代码,进而改变线程正在等待的条件。它必须与 notify() 或 notifyAll() 配合使用,且必须在 synchronized 代码块内调用,否则会抛出 IllegalMonitorStateException

  • Thread.join():用于等待目标线程执行结束。例如,主线程调用 threadA.join() 后,主线程会进入等待状态,直到 threadA 运行终止。其底层是通过 wait() 机制实现的,因此也会释放锁。

  • Thread.yield():向线程调度器提示当前线程愿意让出当前使用的 CPU 执行权。注意,这仅仅是一个提示,调度器可以完全忽略它。调用 yield() 方法不会释放持有的锁。该方法极少使用,通常仅用于调试或测试目的。

二、对线程安全的理解

线程安全的核心在于:当多个线程并发访问同一个共享资源时,无论操作系统的线程调度策略如何,也无论线程的执行顺序如何交织,程序都能表现出正确的行为,且无需调用方进行额外的同步协调。其本质是保障数据的一致性,确保多线程并发访问共享数据的结果与单线程顺序访问的结果一致。

在 Java 中,主要的内存区域如堆(Heap)和方法区(Method Area)是线程共享的,因此是线程安全问题的高发区域。实现线程安全主要有以下几种策略:

  1. 互斥同步(阻塞同步):保证同一时刻只有一个线程能访问共享资源,这是一种悲观策略。

    • synchronized 关键字:Java 语言内置的锁机制,可用于修饰方法或代码块,保证其执行的原子性和可见性。

    • ReentrantLockjava.util.concurrent.locks 包下的一个类,它提供了比 synchronized 更灵活的锁操作,如尝试获取锁、可中断、超时机制、公平锁等。

  2. 非阻塞同步:基于冲突检测的乐观并发策略,通常依赖硬件的 CAS(Compare-And-Swap)指令实现。它在更新数据时先检查数据是否已被其他线程修改,如果没有则更新,否则通常进行重试。

    • 原子类(Atomic Classes):如 AtomicIntegerAtomicLong 等,它们通过 CAS 操作保证了单个变量的原子更新,性能通常优于互斥同步。

  3. 避免共享:从设计上消除共享资源,这是最彻底的解决方案。

    • 局部变量:存储于每个线程私有的虚拟机栈中,是天然线程安全的。

    • ThreadLocal:为每个线程提供一个独立的变量副本,将数据与线程绑定,实现了线程内的共享和线程间的隔离。

  4. 使用线程安全容器:直接利用 Java 并发包(java.util.concurrent)提供的、经过高度优化的线程安全容器。

    • 并发容器:如 ConcurrentHashMapCopyOnWriteArrayList 等,它们的内部实现采用了分段锁、CAS、写时复制等高效技术,性能远优于使用 Collections.synchronizedXXX() 包装的旧式容器。

三、Thread 和 Runnable 的区别

两者最核心的区别在于角色和设计理念的不同:

  • Thread 代表的是“线程”这个执行实体本身。通过继承 Thread 类并重写其 run() 方法来定义线程要执行的任务。它本身就是线程对象。

  • Runnable 代表的是“任务”这个逻辑单元。它只是一个实现了 Runnable 接口的 run() 方法的对象。它定义了要执行的工作内容,但其本身并不是一个线程,需要被传递给一个 Thread 实例来执行。

选择 Runnable 的优势

  1. 避免单继承局限:Java 是单继承的,实现了 Runnable 接口的类还可以继承其他类,增加了灵活性。

  2. 实现任务与线程分离:体现了面向对象设计中的“高内聚、低耦合”思想。Runnable 对象只关注任务逻辑,而 Thread 对象负责线程的创建、管理和执行。

  3. 便于共享资源:多个线程可以方便地共享同一个 Runnable 实现类实例的成员变量。例如,在经典的卖票程序中,多个 Thread 可以共享一个实现了 Runnable 的“售票员”对象中的 ticket 变量,从而实现协同工作,thread继承类如果不使用static修饰ticket,每个线程都会有他们自己的ticket

四、对守护线程的理解

守护线程(Daemon Thread)是一种在后台为其他线程提供支持服务的线程。它并非程序核心逻辑的一部分,其生命周期依赖于用户线程(User Thread)。

核心特性:生命周期

  • JVM 是否正常退出的判断条件是:是否还存在任何存活的用户线程

  • 只要还有一个用户线程在运行,JVM 就不会退出。

  • 如果所有用户线程都已结束,JVM 会立即并强制地终止所有仍在运行的守护线程,然后退出。这意味着守护线程的 finally 块中的代码可能没有机会执行。

设置方法
通过调用线程对象的 setDaemon(true) 方法进行设置。此方法必须在调用 start() 方法启动线程之前调用,否则会抛出 IllegalThreadStateException

典型应用场景
由于其“辅助性”和“可随时终止”的特性,守护线程通常用于执行不重要的后台任务:

  • 垃圾回收(Garbage Collection):GC 线程是最典型的守护线程。

  • 监控与管理:如定期检查系统状态、监控内存使用、日志 flush、缓存更新等。

  • 后台计时/事件触发:执行一些周期性的辅助任务。

重要注意事项

  • 不要用于执行关键任务:严禁将执行 I/O 操作(写文件、网络传输)或关键业务逻辑(更新数据库事务)的线程设为守护线程,因为任务中途被强制终止可能导致数据损坏或不一致。

  • finally 块不保证执行:由于是强制终止,守护线程的 finally 代码块中的资源清理操作可能不会执行。

  • 守护线程创建的线程默认为守护线程:由守护线程创建的新线程,默认也是守护线程。

  • java自带的线程框架,会将守护线程转换为用户线程:所以使用后台线程就不要使用java的线程池。

五、ThreadLocal 的原理与使用场景

ThreadLocal 提供了线程局部变量。每个访问该变量的线程都拥有一个独立的变量副本,从而实现了线程隔离,避免了多线程下的共享冲突。它是一种“以空间换时间”的同步方案。

核心原理与数据存储结构

  • 数据并非存储在 ThreadLocal 对象自身中,而是存储在每个线程对象(Thread) 内部。

  • 每个 Thread 对象内部都有一个名为 threadLocals 的成员变量,其类型是 ThreadLocal.ThreadLocalMap

  • ThreadLocalMap 是一个为 ThreadLocal 量身定制的、简单的哈希表结构。

  • 在这个 Map 中:

    • Key:是 ThreadLocal 变量本身(使用弱引用,有助于防止 ThreadLocal 对象本身的内存泄漏)。

    • Value:是用户通过 set() 方法设置的值(使用强引用,这是潜在内存泄漏的主要来源)。

高性能设计

  • 使用特殊的哈希算法(如魔数 0x61c88647,一种斐波那契散列)来极大减少哈希冲突。

  • 使用线性探测法解决冲突,在条目数较少时效率很高。

典型使用场景

  1. 线程上下文管理:在 Web 应用等场景中,跨方法层级透明地传递用户身份信息、事务 ID、语言环境等,无需在每个方法签名中显式传递。

  2. 全局工具类副本:为每个线程提供一份非线程安全但创建成本较高的对象副本(如 SimpleDateFormat),避免同步带来的性能开销。

  3. 数据库连接与事务管理:将数据库连接绑定到当前线程,确保同一事务内的所有操作使用同一个连接。

注意事项:内存泄漏风险

  • 根源:由于 ThreadLocalMap 的 Entry 中 Key 是弱引用,而 Value 是强引用。如果 ThreadLocal 外部强引用被置空,Key 会被 GC 回收,但 Value 不会。如果线程长时间运行(尤其是线程池中的线程)且未被调用 get/set/remove,Value 会一直占用内存。

  • 解决方案:养成良好编程习惯,在使用完 ThreadLocal 变量后,务必显式调用其 remove() 方法,尤其是在线程池环境中,以防止内存泄漏。

ThreadLocal 通过为每个线程分配一个独立的存储空间,优雅地解决了特定场景下的线程安全问题。但它是一把双刃剑,使用时必须谨记最后调用 remove() 来清理资源,避免潜在的内存泄漏问题。

总结

本文对 Java 中常见的几个核心概念进行了梳理与总结,旨在帮助读者更好地区分和理解相关机制。若存在表述不准确之处,欢迎指正与讨论。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐