深入理解Java线程
<h1><strong>引言:为什么我们需要关注线程?</strong></h1><p> 在多核处理器成为主流的今天,我们手中的手机、电脑甚至智能家居设备都拥有多个计算核心。这意味着,如果我们的程序只能在一个核心上运行,就相当于让其他核心"闲置",无法充分发挥硬件性能。想象一下,一个餐厅只有一个服务员,即使厨房有多个厨师,顾客仍然需要排队等待服务——这就是单线程程序的局限性。</p>
<p> 并发编程正是为了解决这个问题而生,而线程作为并发编程的基础单元,理解其工作机制对于编写高效、稳定的应用程序至关重要。作为一名Java开发者,我深刻体会到,对线程的深入理解往往区分了初级和高级程序员。在这篇博客中,我将分享我对Java线程的个人理解,从基础概念到底层实现,希望能为你提供有价值的见解。</p>
<h2><strong>一、线程与进程:本质区别与内在联系</strong></h2>
<p> 在深入线程之前,我们需要从根本上理解线程与进程的区别。这个理解不能停留在表面,而要深入到操作系统层面。</p>
<h3><strong>进程:独立的王国</strong></h3>
<p> 进程可以理解为一个独立的"程序王国",每个王国都有自己独立的领土(内存空间)、资源(打开的文件、网络连接等)和法律(安全上下文)。操作系统为每个进程分配独立的虚拟地址空间,这意味着:</p>
<p> 进程A无法直接访问进程B的内存数据。</p>
<p> 进程崩溃通常不会影响其他进程。</p>
<p> 进程间通信需要特殊机制(管道、消息队列、共享内存等)。</p>
<h3><strong>线程:王国内的协作团队</strong></h3>
<p>线程则是同一个"王国"内的不同"工作团队",它们:</p>
<p> 共享王国的资源(内存、文件描述符等)。</p>
<p> 各自执行不同的任务,但可以协作完成共同目标。</p>
<p> 通信成本极低,因为可以直接访问共享内存。</p>
<p><strong><strong>技术视角的深度理解</strong></strong>:<br> 从操作系统角度看,进程是资源分配的实体,而线程是CPU调度的实体。当我们在Java中创建线程时,实际上是在用户态创建了一个线程控制块,然后通过系统调用在内核态创建对应的内核线程(在Linux中通过clone系统调用)。这就是为什么线程的创建和销毁比进程轻量得多。</p>
<h2><strong>二、Java线程的创建方式:选择背后的思考</strong></h2>
<h3><strong>1. 继承Thread类:简单但不推荐</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>class MyThread extends Thread {</p>
<p> @Override</p>
<p> public void run() {</p>
<p> System.out.println("线程执行: " + Thread.currentThread().getName());</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<p> 这种方式看似简单,但实际上存在设计上的问题。Java是单继承语言,如果继承了Thread类,就无法继承其他类。这违反了"组合优于继承"的设计原则。此外,从任务执行的角度看,线程的执行体(run方法)和线程本身(Thread类)应该是两个关注点,这种方式将它们耦合在一起。</p>
<h3><strong>2. 实现Runnable接口:推荐的标准做法</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>class MyRunnable implements Runnable {</p>
<p> @Override</p>
<p> public void run() {</p>
<p> System.out.println("线程执行: " + Thread.currentThread().getName());</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<p><strong>为什么这是更好的选择?</strong></p>
<p> 符合面向对象设计原则:任务与执行机制分离。</p>
<p> 灵活性:可以继承其他类,实现其他接口。</p>
<p> 可复用性:同一个Runnable实例可以被多个线程共享执行。</p>
<h3><strong>3. 实现Callable接口:需要返回值的场景</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>class MyCallable implements Callable<String> {</p>
<p> @Override</p>
<p> public String call() throws Exception {</p>
<p> return "线程执行结果: " + Thread.currentThread().getName();</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<p><strong>核心价值:</strong><br> Callable的出现解决了Runnable无法返回结果和抛出受检异常的问题。FutureTask作为RunnableFuture接口的实现,既可以被Thread执行,又可以通过Future接口获取结果,这种设计体现了接口隔离原则。</p>
<h3><strong>4. 线程池方式:生产环境的必然选择</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>ExecutorService executor = Executors.newFixedThreadPool(5);</p>
<p>Future<String> future = executor.submit(new MyCallable());</p>
</td>
</tr>
</tbody>
</table>
<p><strong>为什么线程池如此重要?</strong><br>直接创建线程的成本很高,包括:</p>
<p> 内存分配:每个线程需要分配栈空间(默认512KB-1MB)。</p>
<p> 系统调用:需要内核参与线程创建。</p>
<p> 资源管理:线程数量无限制增长会导致系统资源耗尽。</p>
<p> 线程池通过复用线程、控制并发数量、管理生命周期,解决了这些问题。</p>
<h2><strong>三、线程状态与生命周期:状态机的艺术</strong></h2>
<p> 理解线程的状态转换不仅仅是记住几个状态名称,而是要理解每个状态转换的条件和意义。</p>
<h3><strong>状态转换的深度解析</strong></h3>
<p><strong>NEW → RUNNABLE:</strong>(线程生命开始)<strong><br> </strong>当调用start()方法时,线程从NEW状态进入RUNNABLE状态。这里有个重要细节:start()方法只能调用一次,否则会抛出IllegalThreadStateException。这是因为线程的生命周期是不可逆的。</p>
<p><strong>RUNNABLE → BLOCKED:</strong>(锁竞争导致)<br> 这种情况通常发生在 synchronized 同步块上。当线程A持有锁,线程B尝试获取同一个锁时,线程B就会进入BLOCKED状态。这里的关键理解是:BLOCKED状态只与同步的monitor锁相关。</p>
<p><strong>RUNNABLE → WAITING:</strong>(主动等待)<br>有三种方法会导致这种转换:</p>
<p> Object.wait():释放锁并等待,需要其他线程调用notify()/notifyAll()</p>
<p> Thread.join():等待目标线程终止</p>
<p> LockSupport.park():底层并发工具使用</p>
<p><strong>RUNNABLE → TIMED_WAITING:</strong>(主动等待)<strong><br> </strong>与WAITING类似,但带有超时时间。这是为了避免永久等待导致的死锁。</p>
<p>实际开发中的意义:<br> 理解这些状态转换对于调试多线程问题至关重要。当线程出现问题时,我们可以通过jstack等工具查看线程状态,快速定位问题原因。</p>
<h2><strong>四、线程同步与线程安全:秩序的艺术</strong></h2>
<h3><strong>可见性、原子性、有序性</strong></h3>
<p>在深入同步机制前,必须理解并发编程的三个核心问题:</p>
<p><strong> 可见性</strong>:一个线程对共享变量的修改,其他线程能够立即看到。由于CPU缓存的存在,线程可能读取到过期的数据。</p>
<p><strong> 原子性</strong>:一个或多个操作要么全部执行成功,要么全部不执行,不会出现中间状态。</p>
<p><strong> 有序性</strong>:程序执行的顺序按照代码的先后顺序执行。由于指令重排序的存在,实际执行顺序可能与代码顺序不同。</p>
<h3><strong>synchronized的深度理解</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>public class SynchronizedDemo {</p>
<p> // 实例同步方法:锁是当前对象实例</p>
<p> public synchronized void instanceMethod() {</p>
<p> // 临界区</p>
<p> }</p>
<p> </p>
<p> // 静态同步方法:锁是当前类的Class对象</p>
<p> public static synchronized void staticMethod() {</p>
<p> // 临界区</p>
<p> }</p>
<p> </p>
<p> // 同步代码块:可以指定任意对象作为锁</p>
<p> public void someMethod() {</p>
<p> synchronized(this) {</p>
<p> // 临界区</p>
<p> }</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<p><strong>synchronized的实现原理:</strong></p>
<p> 在字节码层面,通过monitorenter和monitorexit指令实现。</p>
<p> 每个对象都有一个monitor(监视器锁)与之关联。</p>
<p> 锁具有可重入性:同一个线程可以多次获取同一把锁。</p>
<h3><strong>ReentrantLock:更灵活的锁机制</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>public class ReentrantLockDemo {</p>
<p> private final ReentrantLock lock = new ReentrantLock(true); // 公平锁</p>
<p> </p>
<p> public void performTask() {</p>
<p> lock.lock();// 可以在这里使用lockInterruptibly()支持中断</p>
<p> try {</p>
<p> // 临界区</p>
<p> } finally {</p>
<p> lock.unlock(); // 必须在finally块中释放锁</p>
<p> }</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<p><strong>与synchronized的对比:</strong></p>
<table>
<thead>
<tr><th>
<p>特性</p>
</th><th>
<p>synchronized</p>
</th><th>
<p>ReentrantLock</p>
</th></tr>
</thead>
<tbody>
<tr>
<td>
<p>实现机制</p>
</td>
<td>
<p>JVM内置</p>
</td>
<td>
<p>JDK实现</p>
</td>
</tr>
<tr>
<td>
<p>锁获取</p>
</td>
<td>
<p>自动获取释放</p>
</td>
<td>
<p>手动控制</p>
</td>
</tr>
<tr>
<td>
<p>可中断</p>
</td>
<td>
<p>不支持</p>
</td>
<td>
<p>支持</p>
</td>
</tr>
<tr>
<td>
<p>公平性</p>
</td>
<td>
<p>非公平</p>
</td>
<td>
<p>可选择公平或非公平</p>
</td>
</tr>
<tr>
<td>
<p>条件变量</p>
</td>
<td>
<p>单一</p>
</td>
<td>
<p>多个</p>
</td>
</tr>
</tbody>
</table>
<h3><strong>volatile关键字:轻量级的同步</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>public class VolatileExample {</p>
<p> private volatile boolean shutdown = false;</p>
<p> </p>
<p> public void shutdown() {</p>
<p> shutdown = true; // 写操作具有原子性和可见性</p>
<p> }</p>
<p> </p>
<p> public void doWork() {</p>
<p> while (!shutdown) { // 读操作总能获取最新值</p>
<p> // 执行任务</p>
<p> }</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<p><strong>volatile的语义:</strong></p>
<p> 可见性:对volatile变量的写操作会立即刷新到主内存。</p>
<p> 有序性:禁止指令重排序(内存屏障)。</p>
<p> 不保证原子性:复合操作(如i++)仍然需要同步。</p>
<p><strong>适用场景:</strong></p>
<p> 状态标志位。</p>
<p> 双重检查锁定模式。</p>
<p> 观察者模式中的状态发布。</p>
<h2><strong>五、线程间通信:协作的智慧</strong></h2>
<h3><strong>wait/notify机制:经典的线程协作</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>public class WaitNotifyDemo {</p>
<p> private boolean condition = false;</p>
<p> </p>
<p> public synchronized void waitForCondition() throws InterruptedException {</p>
<p> // 必须使用while循环检查条件,避免虚假唤醒</p>
<p> while (!condition) {</p>
<p> wait(); // 释放锁并等待</p>
<p> }</p>
<p> // 条件满足,执行后续操作</p>
<p> doSomething();</p>
<p> }</p>
<p> </p>
<p> public synchronized void signalCondition() {</p>
<p> condition = true;</p>
<p> notifyAll(); // 通知所有等待线程</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<p><strong>wait/notify的使用要点:</strong></p>
<p> 必须在同步方法或同步块中调用。</p>
<p> 总是使用while循环检查条件,避免虚假唤醒。</p>
<p> 优先使用notifyAll()而不是notify(),避免信号丢失。</p>
<h3><strong>Condition接口:更精确的线程控制</strong></h3>
<table>
<tbody>
<tr>
<td>
<p>public class ConditionDemo {</p>
<p> private final Lock lock = new ReentrantLock();</p>
<p> private final Condition condition = lock.newCondition();</p>
<p> private boolean ready = false;</p>
<p> </p>
<p> public void await() throws InterruptedException {</p>
<p> lock.lock();</p>
<p> try {</p>
<p> while (!ready) {</p>
<p> condition.await(); // 等待条件</p>
<p> }</p>
<p> } finally {</p>
<p> lock.unlock();</p>
<p> }</p>
<p> }</p>
<p> </p>
<p> public void signal() {</p>
<p> lock.lock();</p>
<p> try {</p>
<p> ready = true;</p>
<p> condition.signal(); // 通知等待线程</p>
<p> } finally {</p>
<p> lock.unlock();</p>
<p> }</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<p><strong>Condition的优势:</strong></p>
<p> 一个锁可以关联多个Condition。</p>
<p> 支持更灵活的等待条件。</p>
<p> 可以精确唤醒特定类型的等待线程。</p>
<h2><strong>六、线程池的核心原理:池化技术的典范</strong></h2>
<h3><strong>线程池的架构设计</strong></h3>
<p><strong>线程池采用了生产者-消费者模式:</strong></p>
<p> 生产者:提交任务的线程</p>
<p> 消费者:工作线程</p>
<p> 缓冲区:工作队列</p>
<table>
<tbody>
<tr>
<td>
<p>public class ThreadPoolAnatomy {</p>
<p> // ThreadPoolExecutor的核心构造参数</p>
<p> ThreadPoolExecutor executor = new ThreadPoolExecutor(</p>
<p> 5, // 核心线程数:池中保持的线程数量</p>
<p> 10, // 最大线程数:池中允许的最大线程数量</p>
<p> 60L, // 保持时间:超出核心线程数的空闲线程存活时间</p>
<p> TimeUnit.SECONDS, // 时间单位</p>
<p> new LinkedBlockingQueue<>(100), // 工作队列:存储待执行任务</p>
<p> Executors.defaultThreadFactory(), // 线程工厂:创建新线程</p>
<p> new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:无法处理任务时的策略</p>
<p>);</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<h3><strong>任务执行流程的深度解析</strong></h3>
<p><strong>任务提交</strong>:调用execute()或submit()方法</p>
<p><strong>核心线程检查</strong>:如果当前线程数 < corePoolSize,创建新线程</p>
<p><strong>队列检查</strong>:如果线程数 ≥ corePoolSize,尝试将任务放入队列</p>
<p><strong>最大线程检查</strong>:如果队列已满且线程数 < maximumPoolSize,创建新线程</p>
<p><strong>拒绝策略</strong>:如果队列已满且线程数 ≥ maximumPoolSize,执行拒绝策略</p>
<p><strong>这个流程的重要性在于</strong>:它决定了线程池的行为特性。理解这个流程有助于我们根据具体场景配置合适的参数。</p>
<h3><strong>拒绝策略的四种选择</strong></h3>
<p><strong>AbortPolicy(默认):</strong>抛出RejectedExecutionException。</p>
<p><strong>CallerRunsPolicy:</strong>由调用者线程执行任务。</p>
<p><strong>DiscardPolicy:</strong>静默丢弃任务。</p>
<p><strong>DiscardOldestPolicy:</strong>丢弃队列中最老的任务,然后重试。</p>
<h2><strong>七、常见问题与最佳实践:经验的结晶</strong></h2>
<h3><strong>死锁:四大必要条件</strong></h3>
<p><strong>死锁的发生需要同时满足四个条件:</strong></p>
<p> 互斥条件:资源不能被共享</p>
<p> 持有并等待:线程持有资源并等待其他资源</p>
<p> 不可剥夺:资源只能由持有线程释放</p>
<p> 循环等待:存在线程资源的循环等待链</p>
<p><strong>预防死锁的策略:</strong></p>
<p> 按固定顺序获取锁</p>
<p> 使用tryLock()带有超时机制</p>
<p> 使用更高级的并发工具</p>
<table>
<tbody>
<tr>
<td>
<p>public class DeadlockPrevention {</p>
<p> private final Object lock1 = new Object();</p>
<p> private final Object lock2 = new Object();</p>
<p> </p>
<p> public void method1() {</p>
<p> synchronized(lock1) {</p>
<p> // 一些操作</p>
<p> synchronized(lock2) {</p>
<p> // 临界区</p>
<p> }</p>
<p> }</p>
<p> }</p>
<p> </p>
<p> public void method2() {</p>
<p> synchronized(lock1) {// 使用与method1相同的锁顺序</p>
<p> // 一些操作</p>
<p> synchronized(lock2) {</p>
<p> // 临界区</p>
<p> }</p>
<p> }</p>
<p>}</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<h3><strong>上下文切换:看不见的性能杀手</strong></h3>
<p><strong>上下文切换的成本包括:</strong></p>
<p> 直接成本:保存和恢复线程上下文。</p>
<p> 间接成本:缓存失效、TLB刷新。</p>
<p><strong>优化建议:</strong></p>
<p> 避免创建过多线程。</p>
<p> 使用线程池复用线程。</p>
<p> 减少锁竞争(锁细化、使用并发集合)。</p>
<h3><strong>最佳实践总结</strong></h3>
<p><strong>命名线程:便于调试和监控</strong></p>
<table>
<tbody>
<tr>
<td>
<p>ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()</p>
<p> .setNameFormat("worker-thread-%d")</p>
<p> .build();</p>
</td>
</tr>
</tbody>
</table>
<p><strong>正确处理异常:</strong></p>
<table>
<tbody>
<tr>
<td>
<p>executor.submit(() -> {</p>
<p> try {</p>
<p> // 任务逻辑</p>
<p> } catch (Exception e) {</p>
<p> // 记录日志,不要吞掉异常</p>
<p> logger.error("Task execution failed", e);</p>
<p>}</p>
<p>});</p>
</td>
</tr>
</tbody>
</table>
<p><strong>资源清理:</strong></p>
<table>
<tbody>
<tr>
<td>
<p>executor.shutdown();</p>
<p>try {</p>
<p> if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {</p>
<p> executor.shutdownNow();</p>
<p>}</p>
<p>} catch (InterruptedException e) {</p>
<p> executor.shutdownNow();</p>
<p>Thread.currentThread().interrupt();</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
<h2><strong>八、Java内存模型(JMM):并发编程的理论基础</strong></h2>
<h3><strong>happens-before关系</strong></h3>
<p>happens-before是JMM的核心概念,它定义了操作之间的可见性关系:</p>
<p><strong> 程序次序规则</strong>:线程内按照代码顺序执行。</p>
<p><strong> 监视器锁规则</strong>:解锁操作happens-before后续的加锁操作。</p>
<p><strong> volatile变量规则</strong>:写操作happens-before后续的读操作。</p>
<p><strong> 线程启动规则</strong>:Thread.start()happens-before线程内的任何操作。</p>
<p><strong> 线程终止规则</strong>:线程中的所有操作happens-before其他线程检测到该线程已经终止。</p>
<h3><strong>内存屏障</strong></h3>
<p>为了实现happens-before关系,JVM在适当的位置插入内存屏障:</p>
<p><strong> LoadLoad屏障</strong>:禁止读操作重排序。</p>
<p><strong> StoreStore屏障</strong>:禁止写操作重排序。</p>
<p><strong> LoadStore屏障</strong>:禁止读后写重排序。</p>
<p><strong> StoreLoad屏障</strong>:禁止写后读重排序。</p>
<p>理解这些底层机制有助于我们写出正确性更高的并发代码。</p>
<p><em>本文基于个人实践经验和深入研究总结而成,技术观点如有不同见解欢迎交流讨论。</em></p><br><br>
来源:https://www.cnblogs.com/syf0824/p/19163727
頁:
[1]