并发编程之《彻底搞懂Java线程》

简介: 本文系统讲解Java并发编程核心知识,涵盖线程概念、创建方式、线程安全、JUC工具集(线程池、并发集合、同步辅助类)及原子类原理,帮助开发者构建完整的并发知识体系。
  • 引言
  • 一、核心概念:线程是什么?
  • 二、如何创建并运行一个线程?
  • 三、线程安全:共享资源的“修罗场”
  • 四、JUC并发工具集(重中之重)
  • 五、原子类:无锁的线程安全
  • 总结与展望
  • 互动环节

引言

在现代多核CPU的背景下,并发编程是挖掘机器性能、提升应用吞吐量的关键手段。然而,它也是一把“双刃剑”,在带来性能提升的同时,也引入了诸如线程安全、死锁、上下文切换开销等一系列复杂问题。Java作为一门企业级语言,从最初的 synchronized 关键字,到强大的 java.util.concurrent (JUC) 包,为我们提供了一整套强大的并发工具。

本文将从线程的基本概念讲起,逐步深入到JUC的核心组件,旨在帮助你构建一个清晰、系统的Java并发知识体系。

一、核心概念:线程是什么?

在深入细节之前,我们先统一一下认知。

  • 进程 vs 线程
  • 进程:可以理解为一个独立的应用程序。例如,你同时打开的Chrome浏览器和IDEA开发工具就是两个进程。每个进程都有自己独立的内存空间,互不干扰。
  • 线程:是进程中的执行单元,也称为“轻量级进程”。一个进程可以包含多个线程,所有线程共享进程的内存空间(如堆、方法区)。这就好比一个工厂(进程)里有多个流水线(线程),它们共享工厂的电力、原料仓库等资源。
  • 上下文切换
    单核CPU在同一时刻只能执行一个线程。为了让用户感觉多个线程在同时执行,CPU需要通过分配时间片来轮流执行各个线程。当一个线程的时间片用完或被高优先级线程抢占时,就需要保存当前线程的状态(如程序计数器、寄存器信息),然后加载另一个线程的状态,这个过程就是
    上下文切换。频繁的上下文切换会消耗大量资源。
  • 线程的生命周期
    线程从创建到销毁,会经历多种状态:
  • NEW(新建):线程被创建,但尚未调用 start() 方法。
  • RUNNABLE(可运行):调用了 start() 方法,线程已在JVM中,等待操作系统分配CPU时间片。它可能正在运行,也可能在就绪队列中等待。
  • BLOCKED(阻塞):线程试图获取一个内部对象锁(非JUC中的锁),而该锁正被其他线程持有。
  • WAITING(等待):线程进入等待状态,需要被其他线程显式地唤醒(如调用 Object.notify()LockSupport.unpark())。
  • TIMED_WAITING(超时等待):线程进入等待状态,但会在指定的时间后自动唤醒(如 Thread.sleep(long millis)Object.wait(long timeout))。
  • TERMINATED(终止):线程已执行完毕。

二、如何创建并运行一个线程?

Java提供了三种主要的创建线程的方式:

1.继承 Thread
重写 run() 方法,然后创建子类实例并调用其 start() 方法。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程运行中: " + Thread.currentThread().getName());
    }
}
// 使用
MyThread thread = new MyThread();
thread.start(); // 注意:是start()而不是run(),run()只是普通方法调用

2.实现 Runnable 接口(更推荐)
实现 Runnable 接口的 run() 方法,然后将 Runnable 实例作为参数传递给 Thread 构造函数。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行中: " + Thread.currentThread().getName());
    }
}
// 使用
Thread thread = new Thread(new MyRunnable());
thread.start();

优点:避免了单继承的局限性,更适合资源共享。

3.实现 Callable 接口
CallableRunnable 类似,但关键区别在于它
有返回值,并且可以抛出异常

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        return "任务执行结果";
    }
}
// 使用
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
// 获取返回值(会阻塞当前线程直到计算完成)
String result = futureTask.get();
System.out.println(result);

三、线程安全:共享资源的“修罗场”

当多个线程共享同一份数据,并且至少有一个线程会对数据进行操作时,如果不采取任何保护措施,就极易产生线程安全问题。

示例:一个经典的线程不安全案例

public class UnsafeCounter {
    private int count = 0;
    
    public void add() {
        count++; // count = count + 1;
    }
    
    public int get() {
        return count;
    }
}

count++ 看似是一个操作,但实际上是一个“读取-修改-写入”的三步操作。在多线程环境下,可能会发生线程A刚读取完值,CPU就被线程B抢走,B也读取了同样的值并完成加1写入,随后A又用自己的旧值加1后写入,最终导致两次加法操作只生效了一次。

解决方案主要有以下几种:

1.synchronized关键字

synchronized 是Java提供的内置锁,用于保证代码块的互斥访问。同一时刻,只有一个线程能持有某个对象的锁,从而进入被synchronized保护的代码块。

  • 同步代码块:需要显式指定锁对象。
  • public void add() { synchronized (this) { // 以当前对象实例作为锁 count++; } }
  • 同步实例方法:锁是当前对象实例 (this)。
  • public synchronized void add() { // 锁是this count++; }
  • 同步静态方法:锁是当前类的 Class 对象。
  • public static synchronized void add() { // 锁是UnsafeCounter.class // ... }

2.volatile关键字

volatile 是一个轻量级的同步机制,它主要解决的是可见性有序性问题,但不保证原子性

  • 可见性:当一个线程修改了 volatile 修饰的变量,新值会立即被刷新到主内存中。当其他线程需要读取这个变量时,它会从主内存重新读取新值,而不是使用自己工作内存中的旧值。
  • 有序性:禁止指令重排序优化。

适用场景:通常用于标志位(如 boolean flag),一个线程写,多个线程读。

public class VolatileExample {
    private volatile boolean flag = false; // 使用volatile保证可见性
    public void writer() {
        flag = true; // 写操作
    }
    public void reader() {
        if (flag) { // 读操作,总能读到最新的值
            // do something
        }
    }
}

3.Lock接口 (如ReentrantLock)


java.util.concurrent.locks.Lock
接口提供了比 synchronized
更灵活、更强大的锁操作。

其实现类 ReentrantLock(可重入锁)是最常用的。

优势

  • 尝试非阻塞获取锁tryLock()
  • 可中断的获取锁lockInterruptibly(),等待锁的线程可以被中断。
  • 超时获取锁tryLock(long time, TimeUnit unit)
  • 支持公平锁:构造函数传入 true 可以创建一个公平锁(等待时间最长的线程优先获得锁),默认为非公平锁,吞吐量更高。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SafeCounterWithLock {
    private int count = 0;
    private final Lock lock = new ReentrantLock(); // 创建Lock实例
    
    public void add() {
        lock.lock(); // 获取锁
        try {
            count++; // 临界区代码
        } finally {
            lock.unlock(); // 必须在finally块中释放锁,防止异常导致死锁
        }
    }
}

四、JUC并发工具集(重中之重)

java.util.concurrent (JUC) 包提供了大量高效、实用的并发工具类,极大地简化了并发编程。

1.ExecutorService线程池

“线程池”顾名思义,就是预先创建好一批线程,放在一个“池子”里管理。有任务需要执行时,就从池子里拿一个空闲线程来执行,任务完成后线程不销毁,而是回到池中等待下一个任务。这避免了频繁创建和销毁线程的巨大开销。

核心实现类:ThreadPoolExecutor

理解其构造参数至关重要:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

参数名

含义

corePoolSize

核心线程数。即使线程空闲,也不会被回收(除非设置了allowCoreThreadTimeOut)。

maximumPoolSize

最大线程数。线程池能容纳的最大线程数。

keepAliveTime

空闲线程存活时间。当线程数超过核心线程数时,多余的空闲线程在等待新任务的最长时间,超过则被回收。

unit

keepAliveTime 的时间单位。

workQueue

工作队列。用于保存等待执行的任务的阻塞队列。

threadFactory

线程工厂。用于创建新线程,可以自定义线程名、优先级等。

handler

拒绝策略。当线程池和队列都已满时,如何处理新提交的任务。

工作流程

  1. 提交任务。
  2. 如果当前运行线程数 < corePoolSize,则创建新线程执行任务。
  3. 否则,将任务放入 workQueue
  4. 如果队列已满,且运行线程数 < maximumPoolSize,则创建新线程执行任务。
  5. 如果队列已满,且运行线程数已达 maximumPoolSize,则触发拒绝策略

通常不直接 new ThreadPoolExecutor,而是使用 Executors 工具类提供的工厂方法(注意:Executors 提供的某些方法可能有隐患,如无界队列可能导致OOM,需根据场景选择):

// 固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
// 单线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 可缓存的线程池(线程数可弹性伸缩)
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 提交任务
future = executor.submit(myCallableTask);
// 优雅关闭
executor.shutdown();

2. 并发集合 (Concurrent Collections)

传统的 HashMap, ArrayList 等集合类不是线程安全的。JUC提供了一系列高性能的线程安全集合。

  • ConcurrentHashMap: 并发版的 HashMap。采用分段锁(JDK7)或 CAS + synchronized(JDK8及以后)实现高并发读写,性能远高于使用 Collections.synchronizedMap() 包装的HashMap。
  • CopyOnWriteArrayList: 并发版的 ArrayList写时复制——每次修改(增、删、改)操作时,都会复制底层数组,在新数组上操作,完成后将引用指向新数组。读操作完全无锁,性能极高。适用于读多写少的场景(如监听器列表)。

3. 同步辅助类

JUC提供了几个强大的工具类,来协调多个线程之间的控制流。

CountDownLatch(倒计时门闩)

允许一个或多个线程等待其他一组线程完成操作。

构造时传入一个计数器。等待的线程调用 await() 方法阻塞,其他线程完成工作后调用 countDown() 方法使计数器减1。当计数器减为0时,所有等待的线程被唤醒。

典型场景:主线程等待所有子线程完成任务后再继续。

// 模拟:主线程等待5个Worker线程完成任务
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5); // 计数器初始为5
        
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 完成任务");
                latch.countDown(); // 计数器减1
            }, "Worker-" + i).start();
        }
        
        latch.await(); // 主线程在此等待,直到计数器为0
        System.out.println("所有Worker任务已完成,主线程继续执行");
    }
}

CyclicBarrier(循环栅栏)

一组线程相互等待,直到所有线程都到达一个公共的屏障点,然后才能继续执行。计数器可以重置后重复使用

构造时传入参与线程的数量和一个可选的 Runnable 任务(在所有线程到达屏障后执行)。每个线程调用 await() 方法通知屏障自己已到达,然后被阻塞。当最后一个线程到达后,屏障开放,所有被阻塞的线程继续执行,并可执行可选的屏障动作。

典型场景:多线程计算数据,最后合并计算结果。

// 模拟:3个士兵线程集合完毕后才能一起行动
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("所有士兵已集合完毕,出发!"); // 屏障动作
        });
        
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 到达集合点");
                    barrier.await(); // 等待其他士兵
                    // 屏障开放后,所有线程同时继续执行
                    System.out.println(Thread.currentThread().getName() + " 开始行动");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Soldier-" + i).start();
        }
    }
}

Semaphore(信号量)

用来控制同时访问特定资源的线程数量。它通过发放“许可”来管理。

构造时传入许可的数量。线程通过 acquire() 方法获取许可,如果许可已发完,则线程阻塞。使用完资源后,通过 release() 方法释放许可。

典型场景:数据库连接池、流量控制。

// 模拟:一个只有3个许可的厕所
public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3); // 3个许可
        
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取许可
                    System.out.println(Thread.currentThread().getName() + " 占了一个坑位");
                    Thread.sleep(2000); // 模拟使用时间
                    System.out.println(Thread.currentThread().getName() + " 释放坑位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放许可
                }
            }, "Person-" + i).start();
        }
    }
}

三者的简单区别

  • CountDownLatch: 一个线程等多个线程。(一次性)
  • CyclicBarrier多个线程相互等。(可循环)
  • Semaphore限制同时执行的线程数量

五、原子类:无锁的线程安全

JUC提供了一系列原子类(如 AtomicInteger, AtomicLong, AtomicReference),它们通过无锁的方式实现了线程安全的原子操作。其核心原理是 CAS (Compare-And-Swap)

CAS 是一种乐观锁机制。它包含三个操作数:

  1. 内存位置 (V)
  2. 期望的原值 (A)
  3. 新值 (B)

CAS的原理是:只有当 V 的值等于 A 时,才会用 B 去更新 V 的值;否则,什么都不做(或者重试)。整个操作是一个原子指令,由CPU保证其原子性。

import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0); // 初始化原子整型
    
    public void add() {
        count.incrementAndGet(); // 原子性的 ++i
        // 底层实现类似于:
        // int current;
        // do {
        //     current = get();
        // } while (!compareAndSet(current, current + 1));
    }
    
    public int get() {
        return count.get();
    }
}

优点:性能通常比锁更高,因为避免了线程挂起和上下文切换。
缺点:存在 ABA问题(可以通过 AtomicStampedReference 加版本号解决),以及自旋循环可能长时间占用CPU。

总结与展望

本文系统性地梳理了Java线程与并发编程的核心知识:

  1. 基础:理解了线程、进程、生命周期等概念。
  2. 创建:掌握了三种创建线程的方式,推荐使用 RunnableCallable
  3. 安全:学会了使用 synchronized, volatile, Lock 来解决线程安全问题。
  4. 工具:深入学习了JUC的核心——线程池、并发集合和三大同步工具类 (CountDownLatch, CyclicBarrier, Semaphore)。
  5. 无锁:了解了原子类和CAS无锁编程的思想。

并发编程的世界远不止于此。要成为一名真正的并发专家,你还可以继续探索:

  • 更底层的原理:JMM(Java内存模型)、happens-before原则、锁优化(自旋锁、锁消除、锁粗化、偏向锁、轻量级锁)。
  • 更高级的工具CompletableFuture(异步编程)、Fork/Join 框架(分治并行任务)。
  • 问题排查:如何使用 jstack 等工具诊断死锁、活锁、资源耗尽等问题。

并发编程复杂但充满魅力,是区分Java程序员水平高低的重要标尺。希望本文能为你打下坚实的基础!

相关文章
|
22天前
|
负载均衡 Java API
《深入理解Spring》Spring Cloud 构建分布式系统的微服务全家桶
Spring Cloud为微服务架构提供一站式解决方案,涵盖服务注册、配置管理、负载均衡、熔断限流等核心功能,助力开发者构建高可用、易扩展的分布式系统,并持续向云原生演进。
|
18天前
|
安全 前端开发 Java
Spring Boot 过滤器(Filter)详解
本文详解Spring Boot中过滤器的原理与实践,涵盖Filter接口、执行流程、@Component与FilterRegistrationBean两种实现方式、执行顺序控制及典型应用场景如日志记录、权限验证。对比拦截器,突出其在Servlet容器层的通用性与灵活性,助力构建高效稳定的Web应用。
315 1
|
29天前
|
人工智能 自然语言处理 JavaScript
利用MCP Server革新软件测试:更智能、更高效的自动化
MCP Server革新软件测试:通过标准化协议让AI实时感知页面结构,实现自然语言驱动、自适应维护的自动化测试,大幅提升效率,降低脚本开发与维护成本,推动测试左移与持续测试落地。
|
26天前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
安全 Java 编译器
多线程(看这一篇就够了,超详细,满满的干货)
多线程(看这一篇就够了,超详细,满满的干货)
986 2
|
26天前
|
存储 算法 安全
《Java集合核心HashMap:深入剖析其原理、陷阱与性能优化》
HashMap是Java中最常用的Map实现,基于哈希表提供近乎O(1)的存取效率。其核心为“数组+链表+红黑树”结构,通过扰动哈希、&运算索引、扩容机制等实现高效操作。但线程不安全,需注意Key的不可变性与合理初始化容量。深入理解其原理,有助于写出高性能代码,避免常见陷阱。
|
2月前
|
人工智能 运维 安全
配置驱动的动态 Agent 架构网络:实现高效编排、动态更新与智能治理
本文所阐述的配置驱动智能 Agent 架构,其核心价值在于为 Agent 开发领域提供了一套通用的、可落地的标准化范式。
519 52
|
17天前
|
监控 JavaScript 编译器
从“天书”到源码:HarmonyOS NEXT 崩溃堆栈解析实战指南
本文详解如何利用 hiAppEvent 监控并获取 sourcemap、debug so 等核心产物,剖析了 hstack 工具如何将混淆的 Native 与 ArkTS 堆栈还原为源码,助力开发者掌握异常分析方法,提升应用稳定性。
257 31
|
26天前
|
存储 安全 Java
JUC系列之《深入理解synchronized:Java并发编程的基石 》
本文深入解析Java中synchronized关键字的使用与原理,涵盖其三种用法、底层Monitor机制、锁升级过程及JVM优化,并对比Lock差异,结合volatile应用场景,全面掌握线程安全核心知识。
|
26天前
|
Java API 开发者
告别“线程泄露”:《聊聊如何优雅地关闭线程池》
本文深入讲解Java线程池优雅关闭的核心方法与最佳实践,通过shutdown()、awaitTermination()和shutdownNow()的组合使用,确保任务不丢失、线程不泄露,助力构建高可靠并发应用。