并发编程之《彻底搞懂Java线程》_java多线程并发解决方案详解
wptr33 2025-09-19 03:56 51 浏览
目录
- 引言
- 一、核心概念:线程是什么?
- 二、如何创建并运行一个线程?
- 三、线程安全:共享资源的“修罗场”
- 四、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并发编程中踩过哪些坑?或者对文中的哪个知识点有独特的见解?欢迎在评论区分享你的经验和思考!
相关推荐
- oracle数据导入导出_oracle数据导入导出工具
-
关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...
- 继续学习Python中的while true/break语句
-
上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个else解...
- python continue和break的区别_python中break语句和continue语句的区别
-
python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...
- 简单学Python——关键字6——break和continue
-
Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...
- 2-1,0基础学Python之 break退出循环、 continue继续循环 多重循
-
用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...
- Python 中 break 和 continue 傻傻分不清
-
大家好啊,我是大田。今天分享一下break和continue在代码中的执行效果是什么,进一步区分出二者的区别。一、continue例1:当小明3岁时不打印年龄,其余年龄正常循环打印。可以看...
- python中的流程控制语句:continue、break 和 return使用方法
-
Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...
- L017:continue和break - 教程文案
-
continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...
- 作为前端开发者,你都经历过怎样的面试?
-
已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...
- 面试被问 const 是否不可变?这样回答才显功底
-
作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...
- 2023金九银十必看前端面试题!2w字精品!
-
导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。答案:CSS的盒模型是用于布局和定位元素的概念。它由内容区域...
- 前端面试总结_前端面试题整理
-
记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...
- 由浅入深,66条JavaScript面试知识点(七)
-
作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录由浅入深,66条JavaScript面试知识点(一)由浅入深,66...
- 2024前端面试真题之—VUE篇_前端面试题vue2020及答案
-
添加图片注释,不超过140字(可选)1.vue的生命周期有哪些及每个生命周期做了什么?beforeCreate是newVue()之后触发的第一个钩子,在当前阶段data、methods、com...
- 今年最常见的前端面试题,你会做几道?
-
在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...
- 一周热门
- 最近发表
- 标签列表
-
- 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)
