Java核心概念精讲:线程安全,守护线程,ThreadLocal 的原理与使用场景等(21-25)
本文介绍了Java多线程编程中的核心概念:1. sleep()、wait()、join()和yield()的区别,重点解析了锁池与等待池的概念;2. 线程安全的实现策略,包括互斥同步、非阻塞同步和避免共享等方法;3. Thread和Runnable的区别,说明Runnable在任务与线程分离方面的优势;4. 守护线程的特性、使用场景和注意事项;5. ThreadLocal的原理、使用场景及其内存泄
前言
本文需要有一定的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)是线程共享的,因此是线程安全问题的高发区域。实现线程安全主要有以下几种策略:
-
互斥同步(阻塞同步):保证同一时刻只有一个线程能访问共享资源,这是一种悲观策略。
-
synchronized 关键字:Java 语言内置的锁机制,可用于修饰方法或代码块,保证其执行的原子性和可见性。
-
ReentrantLock:
java.util.concurrent.locks
包下的一个类,它提供了比synchronized
更灵活的锁操作,如尝试获取锁、可中断、超时机制、公平锁等。
-
-
非阻塞同步:基于冲突检测的乐观并发策略,通常依赖硬件的 CAS(Compare-And-Swap)指令实现。它在更新数据时先检查数据是否已被其他线程修改,如果没有则更新,否则通常进行重试。
-
原子类(Atomic Classes):如
AtomicInteger
、AtomicLong
等,它们通过 CAS 操作保证了单个变量的原子更新,性能通常优于互斥同步。
-
-
避免共享:从设计上消除共享资源,这是最彻底的解决方案。
-
局部变量:存储于每个线程私有的虚拟机栈中,是天然线程安全的。
-
ThreadLocal:为每个线程提供一个独立的变量副本,将数据与线程绑定,实现了线程内的共享和线程间的隔离。
-
-
使用线程安全容器:直接利用 Java 并发包(
java.util.concurrent
)提供的、经过高度优化的线程安全容器。-
并发容器:如
ConcurrentHashMap
、CopyOnWriteArrayList
等,它们的内部实现采用了分段锁、CAS、写时复制等高效技术,性能远优于使用Collections.synchronizedXXX()
包装的旧式容器。
-
三、Thread 和 Runnable 的区别
两者最核心的区别在于角色和设计理念的不同:
-
Thread 代表的是“线程”这个执行实体本身。通过继承
Thread
类并重写其run()
方法来定义线程要执行的任务。它本身就是线程对象。 -
Runnable 代表的是“任务”这个逻辑单元。它只是一个实现了
Runnable
接口的run()
方法的对象。它定义了要执行的工作内容,但其本身并不是一个线程,需要被传递给一个Thread
实例来执行。
选择 Runnable 的优势:
-
避免单继承局限:Java 是单继承的,实现了
Runnable
接口的类还可以继承其他类,增加了灵活性。 -
实现任务与线程分离:体现了面向对象设计中的“高内聚、低耦合”思想。
Runnable
对象只关注任务逻辑,而Thread
对象负责线程的创建、管理和执行。 -
便于共享资源:多个线程可以方便地共享同一个
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
,一种斐波那契散列)来极大减少哈希冲突。 -
使用线性探测法解决冲突,在条目数较少时效率很高。
典型使用场景:
-
线程上下文管理:在 Web 应用等场景中,跨方法层级透明地传递用户身份信息、事务 ID、语言环境等,无需在每个方法签名中显式传递。
-
全局工具类副本:为每个线程提供一份非线程安全但创建成本较高的对象副本(如
SimpleDateFormat
),避免同步带来的性能开销。 -
数据库连接与事务管理:将数据库连接绑定到当前线程,确保同一事务内的所有操作使用同一个连接。
注意事项:内存泄漏风险:
-
根源:由于
ThreadLocalMap
的 Entry 中 Key 是弱引用,而 Value 是强引用。如果ThreadLocal
外部强引用被置空,Key 会被 GC 回收,但 Value 不会。如果线程长时间运行(尤其是线程池中的线程)且未被调用get
/set
/remove
,Value 会一直占用内存。 -
解决方案:养成良好编程习惯,在使用完
ThreadLocal
变量后,务必显式调用其remove()
方法,尤其是在线程池环境中,以防止内存泄漏。
ThreadLocal
通过为每个线程分配一个独立的存储空间,优雅地解决了特定场景下的线程安全问题。但它是一把双刃剑,使用时必须谨记最后调用 remove()
来清理资源,避免潜在的内存泄漏问题。
总结:
本文对 Java 中常见的几个核心概念进行了梳理与总结,旨在帮助读者更好地区分和理解相关机制。若存在表述不准确之处,欢迎指正与讨论。
更多推荐
所有评论(0)