Java多线程、线程安全、线程池创建方式
作者:jaysee-sjc
多线程是实现多任务并行处理的核心技术,小到程序的异步执行,大到高并发系统的开发,都离不开多线程。比如我们平时用的软件,一边听歌一边刷评论,就是多线程的典型场景。
一、多线程:
1.1 两个基础概念
简单说:进程是软件的实例,线程是软件里的具体任务。
多线程就是让一个程序同时执行多个任务。
- 进程:运行中的程序,比如打开的微信、IDEA,是操作系统资源分配的基本单位。
- 线程:进程中的最小执行单元,一个进程可以包含多个线程,线程共享进程的资源(内存、文件等),是 CPU 调度的基本单位。
1.2 三种创建方式
Java 中创建多线程有3 种核心方式,各有优缺点,适用于不同场景,入门先掌握这三种即可,其中实现 Runnable 接口是最常用的方式。
方式 1:继承 Thread 类
Thread类是 Java 多线程的核心类,直接继承它,重写run()方法(线程的执行体),调用start()方法启动线程。
核心步骤
- 自定义类继承
Thread类; - 重写
run()方法,编写线程执行逻辑; - 创建自定义线程对象,调用
start()方法启动(注意:不能直接调用 run (),否则就是普通方法)。
代码示例
// 1. 自定义线程类,继承Thread
public class MyThread extends Thread {
// 2. 重写run()方法,线程执行的逻辑写在这里
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// Thread.currentThread().getName() 获取当前线程名
System.out.println(Thread.currentThread().getName() + ":执行了" + i);
}
}
public static void main(String[] args) {
// 3. 创建线程对象,设置线程名
MyThread t1 = new MyThread();
t1.setName("线程1");
MyThread t2 = new MyThread();
t2.setName("线程2");
// 4. 启动线程:调用start()
t1.start();
t2.start();
// 主线程也执行逻辑,对比看多线程效果
for (int i = 0; i < 5; i++) {
System.out.println("主线程:执行了" + i);
}
}
}运行结果
多线程的执行顺序由CPU 调度决定,所以每次运行结果都不一样,体现了线程的随机性:
主线程:执行了0
线程1:执行了0
线程2:执行了0
线程1:执行了1
主线程:执行了1
...
缺点
Java 是单继承,自定义类继承 Thread 后,就不能继承其他类,灵活性差。
方式 2:实现 Runnable 接口
Runnable是线程的任务接口,只定义了一个run()方法,把线程和任务分离,更符合面向对象思想,是开发中最常用的方式。
核心步骤
- 自定义类实现
Runnable接口,重写run()方法; - 创建任务对象;
- 把任务对象传给
Thread类的构造器,创建线程对象; - 调用
start()启动线程。
代码示例
// 1. 自定义任务类,实现Runnable接口
public class MyRunnable implements Runnable {
// 2. 重写run()方法,编写任务逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":执行了" + i);
}
}
public static void main(String[] args) {
// 3. 创建任务对象
MyRunnable task = new MyRunnable();
// 4. 把任务传给Thread,创建线程对象
Thread t1 = new Thread(task, "线程1");
Thread t2 = new Thread(task, "线程2");
// 5. 启动线程
t1.start();
t2.start();
}
}优点
- 避免单继承的限制,自定义类可以继承其他类;
- 线程和任务分离,一个任务可以被多个线程执行,资源复用。
方式 3:实现 Callable 接口
前两种方式的run()方法没有返回值,不能抛出编译时异常,如果需要线程执行后返回结果(比如计算一个数值),就用Callable接口,这是 JDK5 新增的方式。
核心要点
Callable<V>:泛型 V 表示返回值类型,重写call()方法(执行体),有返回值,可抛异常;- 需要配合
FutureTask类使用:封装Callable任务,获取线程执行结果; - 最终还是通过
Thread类启动线程。
代码示例
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 1. 自定义任务类,实现Callable接口,指定泛型为返回值类型(Integer)
public class MyCallable implements Callable<Integer> {
// 2. 重写call()方法,有返回值,可抛异常
@Override
public Integer call() throws Exception {
// 模拟计算:1-5的和
int sum = 0;
for (int i = 1; i <= 5; i++) {
sum += i;
System.out.println(Thread.currentThread().getName() + ":计算中,i=" + i);
}
return sum; // 返回计算结果
}
public static void main(String[] args) throws Exception {
// 3. 创建Callable任务对象
MyCallable callable = new MyCallable();
// 4. 用FutureTask封装任务,用于获取返回值
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 5. 把FutureTask传给Thread,创建线程并启动
Thread t = new Thread(futureTask, "计算线程");
t.start();
// 6. 获取线程执行结果:get()方法会阻塞,直到线程执行完成
Integer result = futureTask.get();
System.out.println(t.getName() + "执行结果:1-5的和=" + result);
}
}运行结果
计算线程:计算中,i=1
计算线程:计算中,i=2
计算线程:计算中,i=3
计算线程:计算中,i=4
计算线程:计算中,i=5
计算线程执行结果:1-5的和=15
优点
- 有返回值,能满足 “线程执行后需要结果” 的场景;
call()方法可以抛出编译时异常,无需手动捕获。
1.3 多线程常用方法
整理了开发中最常用的线程方法,分为线程启动 / 命名、线程休眠 / 等待、线程状态获取三类:
| 方法名 | 作用 | 注意事项 |
|---|---|---|
| start() | 启动线程,JVM 会自动调用run() | 不能重复调用,否则抛异常 |
| run() | 线程执行体,编写业务逻辑 | 直接调用是普通方法,不会启动新线程 |
| setName(String name) | 设置线程名 | 也可以通过 Thread 构造器设置 |
| getName() | 获取线程名 | 默认线程名:Thread-0、Thread-1... |
| sleep(long ms) | 让线程休眠指定毫秒数 | 静态方法,休眠时不会释放锁,会抛出 InterruptedException |
| join() | 让调用该方法的线程先执行完 | 比如 t1.join (),主线程会等 t1 执行完再执行 |
| Thread.currentThread() | 获取当前执行的线程对象 | 静态方法,常用在匿名线程中 |
| isAlive() | 判断线程是否存活 | 启动后、结束前为 true |
常用方法示例:
public class ThreadMethodDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":执行" + i);
}
}, "休眠线程");
t.start();
t.join(); // 主线程等待t执行完再执行
System.out.println("主线程:执行完成,休眠线程已结束");
}
}运行结果
休眠线程:执行0
休眠线程:执行1
休眠线程:执行2
主线程:执行完成,休眠线程已结束
二、线程安全:
2.1 什么是线程安全问题?
当多个线程同时操作同一个共享资源时,会导致数据错乱、结果不符合预期,这就是线程安全问题。
核心条件:
- 多线程环境
- 操作共享资源
- 共享资源的操作是非原子性的(一行代码可能对应多个 CPU 指令)。
2.2 模拟线程安全问题:
假设火车站有 10 张票,3 个窗口同时卖票,模拟多线程售票,会出现超卖(卖成 0 张以下)、重复卖的问题,这是典型的线程安全问题。
代码示例
// 售票任务类,实现Runnable
public class TicketRunnable implements Runnable {
// 共享资源:10张票(多个线程共用这个变量)
private int ticketNum = 10;
@Override
public void run() {
// 循环卖票
while (true) {
// 有票就卖
if (ticketNum > 0) {
try {
Thread.sleep(500); // 模拟选座、打印票的耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了1张票,剩余:" + --ticketNum);
} else {
// 无票就退出
break;
}
}
}
public static void main(String[] args) {
// 一个任务对象,3个线程共享
TicketRunnable task = new TicketRunnable();
new Thread(task, "窗口1").start();
new Thread(task, "窗口2").start();
new Thread(task, "窗口3").start();
}
}运行结果
窗口1卖出了1张票,剩余:9
窗口2卖出了1张票,剩余:8
窗口3卖出了1张票,剩余:7
...
窗口2卖出了1张票,剩余:1
窗口3卖出了1张票,剩余:0
窗口1卖出了1张票,剩余:-1 // 超卖,线程安全问题
问题原因
- 多个线程同时判断
ticketNum > 0,比如票只剩 1 张时,3 个线程都判断为 true,然后依次执行--ticketNum,最终就会卖成 - 1。
2.3 解决线程安全问题:线程同步
解决线程安全问题的核心思路是线程同步,也就是给共享资源的操作代码加锁,让多个线程排队执行这部分代码,而不是同时执行。
Java 中提供了3 种加锁方式:同步代码块、同步方法、Lock 锁,都能解决线程安全问题,下面基于售票案例逐一实现。
方式 1:同步代码块(synchronized)
用synchronized关键字包裹操作共享资源的代码块,指定锁对象,多个线程必须使用同一个锁对象才能实现同步。
语法
synchronized(锁对象) {
// 操作共享资源的代码块(临界区)
}
锁对象要求
- 可以是任意对象,但多个线程的锁对象必须是同一个;
- 推荐使用共享资源本身或this(当前任务对象)作为锁对象。
售票案例改造
public class TicketSyncBlock implements Runnable {
private int ticketNum = 10;
// 锁对象:多个线程共用同一个
private Object lock = new Object();
@Override
public void run() {
while (true) {
// 同步代码块:加锁,操作共享资源的代码排队执行
synchronized (lock) {
if (ticketNum > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了1张票,剩余:" + --ticketNum);
} else {
break;
}
}
}
}
public static void main(String[] args) {
TicketSyncBlock task = new TicketSyncBlock();
new Thread(task, "窗口1").start();
new Thread(task, "窗口2").start();
new Thread(task, "窗口3").start();
}
}运行结果
窗口1卖出了1张票,剩余:9
窗口1卖出了1张票,剩余:8
窗口2卖出了1张票,剩余:7
...
窗口3卖出了1张票,剩余:1
窗口3卖出了1张票,剩余:0
方式 2:同步方法
把操作共享资源的代码提取成一个方法,用synchronized修饰,这个方法就是同步方法,无需手动指定锁对象。
锁对象规则
- 非静态同步方法:锁对象是
this(当前任务对象); - 静态同步方法:锁对象是类的 Class 对象(比如 TicketSyncMethod.class)。
售票案例改造
public class TicketSyncMethod implements Runnable {
private int ticketNum = 10;
@Override
public void run() {
while (true) {
// 调用同步方法
if (!sellTicket()) {
break;
}
}
}
// 同步方法:synchronized修饰,操作共享资源的代码
private synchronized boolean sellTicket() {
if (ticketNum > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出了1张票,剩余:" + --ticketNum);
return true;
} else {
return false;
}
}
public static void main(String[] args) {
TicketSyncMethod task = new TicketSyncMethod();
new Thread(task, "窗口1").start();
new Thread(task, "窗口2").start();
new Thread(task, "窗口3").start();
}
}优点
代码更简洁,无需手动创建锁对象,适合整个方法都是操作共享资源的场景。
方式 3:Lock 锁
synchronized是隐式锁(自动加锁、自动释放锁,代码执行完 / 抛异常都会释放),而Lock是显式锁,需要手动加锁和手动释放锁,灵活性更高,是开发中常用的进阶方式。
Java 中Lock的核心实现类是ReentrantLock(可重入锁)。
核心方法
lock():手动加锁;unlock():手动释放锁(必须放在 finally 中,保证锁一定释放,避免死锁)。
售票案例改造
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable {
private int ticketNum = 10;
// 创建Lock锁对象,可重入锁
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 手动加锁
lock.lock();
try {
// 操作共享资源的代码
if (ticketNum > 0) {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "卖出了1张票,剩余:" + --ticketNum);
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 手动释放锁,放在finally中,保证一定执行
lock.unlock();
}
}
}
public static void main(String[] args) {
TicketLock task = new TicketLock();
new Thread(task, "窗口1").start();
new Thread(task, "窗口2").start();
new Thread(task, "窗口3").start();
}
}优点
- 灵活性高:可以在代码任意位置加锁、释放锁,不像 synchronized 只能包裹代码块 / 修饰方法;
- 支持公平锁 / 非公平锁:创建 ReentrantLock 时传入
true就是公平锁(先等待的线程先执行); - 提供更多方法:比如
tryLock()(尝试加锁,加锁失败不阻塞)。
2.4 三种同步方式对比
| 同步方式 | 锁类型 | 加锁 / 释放 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| 同步代码块 | 隐式锁 | 自动 | 中等 | 部分代码操作共享资源 |
| 同步方法 | 隐式锁 | 自动 | 低 | 整个方法操作共享资源 |
| Lock 锁 | 显式锁 | 手动(finally 释放) | 高 | 复杂的同步场景,需要灵活加锁 / 释放 |
核心总结:解决线程安全问题的本质是让多个线程排队执行共享资源的代码,加锁是实现排队的关键。
三、线程池:
3.1 为什么需要线程池?
线程池就是一个线程的容器,提前创建好若干个线程,任务来了直接分配线程执行,任务执行完线程不销毁,放回线程池复用。就像餐厅的服务员,不会来了一个客人就雇一个服务员,而是提前雇好,客人走了服务员继续服务下一个客人。
手动创建线程虽然简单,但存在很多问题:
- 线程创建和销毁消耗系统资源(CPU、内存);
- 无限制创建线程会导致内存溢出(OOM);
- 手动管理线程的状态(启动、停止)非常繁琐。
3.2 线程池的核心优势
- 线程复用:避免频繁创建销毁线程,减少资源开销;
- 控制线程数量:防止创建过多线程导致内存溢出;
- 提高响应速度:任务来了直接用池中的线程,无需等待线程创建;
- 统一管理:方便对线程进行监控、统计、调优。
3.3 线程池处理两种任务:
线程池可以处理我们之前讲的Runnable 任务(无返回值)和Callable 任务(有返回值),核心步骤都是:
- 创建线程池;
- 提交任务给线程池;
- 关闭线程池(可选,根据业务场景)。
Java 中线程池的核心接口是ExecutorService,下面用最常用的固定线程池演示处理两种任务。
案例 1:线程池处理 Runnable 任务(无返回值)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolRunnable {
public static void main(String[] args) {
// 1. 创建固定线程池:核心线程数为2
ExecutorService pool = Executors.newFixedThreadPool(2);
//使用线程池的实现类ThreadPoolExecutor创建线程池对象。(有七个参数)
//ExecutorService pool = new ThreadPoolExecutor(
// 2,//核心线程数
// 2,//最大线程数
// 0,//线程空闲时间
// TimeUnit.SECONDS,//时间单位
// new ArrayBlockingQueue<>(2),//任务队列
// Executors.defaultThreadFactory(),//线程工厂对象
// new ThreadPoolExecutor.AbortPolicy()//拒绝策略
//);
// 2. 提交Runnable任务给线程池
pool.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "执行Runnable任务:" + i);
}
}
});
pool.submit(() -> { // Lambda简化Runnable
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "执行Lambda任务:" + i);
}
});
// 3. 关闭线程池:shutdown()等待任务执行完再关闭,shutdownNow()立即关闭
pool.shutdown();
}
}案例 2:线程池处理 Callable 任务(有返回值)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolCallable {
public static void main(String[] args) throws Exception {
// 1. 创建固定线程池,核心线程数1
ExecutorService pool = Executors.newFixedThreadPool(1);
// 2. 提交Callable任务,Future接收返回值
Future<Integer> future = pool.submit(() -> {
// 计算1-10的和
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
});
// 3. 获取返回值
Integer result = future.get();
System.out.println("Callable任务执行结果:1-10的和=" + result);
// 4. 关闭线程池
pool.shutdown();
}
}运行结果
Callable任务执行结果:1-10的和=55
3.4 Executors 工具类:
Java 提供了java.util.concurrent.Executors工具类,封装了线程池的创建逻辑,提供了4 种常用的线程池创建方法,满足 90% 的开发需求,入门直接用这个工具类即可。
4 种常用线程池总结
| 线程池方法 | 核心特点 | 核心线程数 | 最大线程数 | 适用场景 |
|---|---|---|---|---|
| newFixedThreadPool(n) | 固定线程池,线程数固定,任务排队 | n | n | 任务量固定,需要稳定执行的场景(比如常规业务处理) |
| newSingleThreadExecutor() | 单线程池,只有 1 个线程 | 1 | 1 | 任务需要按顺序执行的场景 |
| newCachedThreadPool() | 缓存线程池,线程数动态扩容,空闲线程 60 秒销毁 | 0 | Integer.MAX_VALUE | 任务量波动大的场景(比如高并发请求) |
| newScheduledThreadPool(n) | 定时线程池,支持定时 / 延迟执行任务 | n | Integer.MAX_VALUE | 定时任务、延迟任务(比如定时备份、定时统计) |
定时线程池示例
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolDemo {
public static void main(String[] args) {
// 创建定时线程池,核心线程数2
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
// 1. 延迟执行:延迟1秒后执行一次任务
pool.schedule(() -> {
System.out.println(Thread.currentThread().getName() + ":延迟1秒执行");
}, 1, TimeUnit.SECONDS);
// 2. 定时执行:延迟2秒后,每3秒执行一次任务
pool.scheduleAtFixedRate(() -> {
System.out.println(Thread.currentThread().getName() + ":定时执行,每3秒一次");
}, 2, 3, TimeUnit.SECONDS);
}
}运行结果
pool-1-thread-1:延迟1秒执行
pool-1-thread-2:定时执行,每3秒一次
pool-1-thread-2:定时执行,每3秒一次
...
3.5 并发 & 并行
学习多线程,一定会遇到并发和并行,两者都表示 “同时执行”,但本质不同,用通俗的语言和例子讲清楚:
并发(Concurrency)
- 定义:同一时间段内多个任务交替执行,看似同时执行,实际是 CPU 在多个任务之间快速切换;
- 底层:适用于单核 CPU;
- 例子:一个厨师同时做两碗面,先煮第一碗的面,再煮第二碗的面,交替进行。
并行(Parallelism)
- 定义:同一时刻多个任务同时执行,真正的同时执行;
- 底层:适用于多核 CPU,每个核执行一个任务;
- 例子:两个厨师同时做两碗面,各自做各自的,互不干扰。
并发是交替执行,并行是同时执行;Java 多线程在单核 CPU 上是并发,在多核 CPU 上是并行。
四、核心知识点总结
- 多线程创建:继承 Thread(单继承限制)、实现 Runnable(最常用,无继承限制)、实现 Callable(带返回值),启动线程必须调用
start(); - 线程安全:多线程操作共享资源导致数据错乱,解决方式是加锁(同步代码块、同步方法、Lock 锁),核心是让线程排队执行;
- 线程池:Executors 工具类快速创建 4 种常用线程池,处理 Runnable(无返回值)和 Callable(有返回值)任务,优势是线程复用、控制数量;
- 并发 & 并行:并发是单核交替执行,并行是多核同时执行。
到此这篇关于Java多线程、线程安全、线程池创建方式的文章就介绍到这了,更多相关Java多线程 线程安全 线程池内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
