并发编程之《彻底搞懂Java线程》_java多线程并发解决方案详解
wptr33 2025-09-19 03:56 2 浏览
目录
- 引言
- 一、核心概念:线程是什么?
- 二、如何创建并运行一个线程?
- 三、线程安全:共享资源的“修罗场”
- 四、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(终止):线程已执行完毕。
- https://www.baeldung.com/wp-content/uploads/2018/02/Life_cycle_of_a_Thread_in_Java.jpg
(示意图,图片来源于网络)
二、如何创建并运行一个线程?
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 接口
Callable 与 Runnable 类似,但关键区别在于它有返回值,并且可以抛出异常。、
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 | 拒绝策略。当线程池和队列都已满时,如何处理新提交的任务。 |
工作流程:
- 提交任务。
- 如果当前运行线程数 < corePoolSize,则创建新线程执行任务。
- 否则,将任务放入 workQueue。
- 如果队列已满,且运行线程数 < maximumPoolSize,则创建新线程执行任务。
- 如果队列已满,且运行线程数已达 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 是一种乐观锁机制。它包含三个操作数:
- 内存位置 (V)
- 期望的原值 (A)
- 新值 (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线程与并发编程的核心知识:
- 基础:理解了线程、进程、生命周期等概念。
- 创建:掌握了三种创建线程的方式,推荐使用 Runnable 和 Callable。
- 安全:学会了使用 synchronized, volatile, Lock 来解决线程安全问题。
- 工具:深入学习了JUC的核心——线程池、并发集合和三大同步工具类 (CountDownLatch, CyclicBarrier, Semaphore)。
- 无锁:了解了原子类和CAS无锁编程的思想。
并发编程的世界远不止于此。要成为一名真正的并发专家,你还可以继续探索:
- 更底层的原理:JMM(Java内存模型)、happens-before原则、锁优化(自旋锁、锁消除、锁粗化、偏向锁、轻量级锁)。
- 更高级的工具:CompletableFuture(异步编程)、Fork/Join 框架(分治并行任务)。
- 问题排查:如何使用 jstack 等工具诊断死锁、活锁、资源耗尽等问题。
并发编程复杂但充满魅力,是区分Java程序员水平高低的重要标尺。希望本文能为你打下坚实的基础!
互动环节
你在Java并发编程中踩过哪些坑?或者对文中的哪个知识点有独特的见解?欢迎在评论区分享你的经验和思考!
相关推荐
- 高性能并发队列Disruptor使用详解
-
基本概念Disruptor是一个高性能的异步处理框架,是一个轻量的Java消息服务JMS,能够在无锁的情况下实现队列的并发操作Disruptor使用环形数组实现了类似队列的功能,并且是一个有界队列....
- Disruptor一个高性能队列_java高性能队列
-
Disruptor一个高性能队列前言说到队列比较熟悉的可能是ArrayBlockingQueue、LinkedBlockingQueue这两个有界队列,大多应用在线程池中使用能保证线程安全,但其安全性...
- 谈谈防御性编程_防御性策略
-
防御性编程对于程序员来说是一种良好的代码习惯,是为了保护自己的程序在不可未知的异常下,避免带来更大的破坏性崩溃,使得程序在错误发生时,依然能够云淡风轻的处理,但很多程序员入行很多年,写出的代码依然都是...
- 有人敲门,开水开了,电话响了,孩子哭了,你先顾谁?
-
前言哎呀,这种情况你肯定遇到过吧!正在家里忙活着,突然——咚咚咚有人敲门,咕噜咕噜开水开了,铃铃铃电话响了,哇哇哇孩子又哭了...我去,四件事一起来,人都懵了!你说先搞哪个?其实这跟我们写Java多线...
- 面试官:线程池如何按照core、max、queue的执行顺序去执行?
-
前言这是一个真实的面试题。前几天一个朋友在群里分享了他刚刚面试候选者时问的问题:"线程池如何按照core、max、queue的执行循序去执行?"。我们都知道线程池中代码执行顺序是:co...
- 深入剖析 Java 中线程池的多种实现方式
-
在当今高度并发的互联网软件开发领域,高效地管理和利用线程资源是提升程序性能的关键。Java作为一种广泛应用于后端开发的编程语言,为我们提供了丰富的线程池实现方式。今天,就让我们深入探讨Java中...
- 并发编程之《彻底搞懂Java线程》_java多线程并发解决方案详解
-
目录引言一、核心概念:线程是什么?...
- Redis怎么实现延时消息_redis实现延时任务
-
一句话总结Redis可通过有序集合(ZSET)实现延时消息:将消息作为value,到期时间戳作为score存入ZSET。消费者轮询用ZRANGEBYSCORE获取到期消息,配合Lua脚本保证原子性获取...
- CompletableFuture真的用对了吗?盘点它最容易被误用的5个场景
-
在Java并发编程中,CompletableFuture是处理异步任务的利器,但不少开发者在使用时踩过这些坑——线上服务突然雪崩、异常悄无声息消失、接口响应时间翻倍……本文结合真实案例,拆解5个最容易...
- 接口性能优化技巧,有点硬_接口性能瓶颈
-
背景我负责的系统到2021年初完成了功能上的建设,开始进入到推广阶段。随着推广的逐步深入,收到了很多好评的同时也收到了很多对性能的吐槽。刚刚收到吐槽的时候,我们的心情是这样的:...
- 禁止使用这5个Java类,每一个背后都有一段"血泪史"
-
某电商平台的支付系统突然报警:大量订单状态异常。排查日志发现,同一笔订单被重复支付了三次。事后复盘显示,罪魁祸首竟是一行看似无害的SimpleDateFormat代码。在Java开发中,这类因使用不安...
- 无锁队列Disruptor原理解析_无锁队列实现原理
-
队列比较队列...
- Java并发队列与容器_java 并发队列
-
【前言:无论是大数据从业人员还是Java从业人员,掌握Java高并发和多线程是必备技能之一。本文主要阐述Java并发包下的阻塞队列和并发容器,其实研读过大数据相关技术如Spark、Storm等源码的,...
- 线程池工具及拒绝策略的使用_线程池处理策略
-
线程池的拒绝策略若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。...
- 【面试题精讲】ArrayBlockingQueue 和 LinkedBlockingQueue 区别?
-
有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
如何将AI助手接入微信(打开ai手机助手)
-
Java面试必考问题:什么是乐观锁与悲观锁
-
SparkSQL——DataFrame的创建与使用
-
redission YYDS spring boot redission 使用
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)