Java多线程实现方式、线程池、Lock锁
本文为个人总结,如有错误请评论区指出
文章目录
并发和并行
并发:在同一时刻,有多个指令在单个cpu上交替执行
并行:在同一时刻,有多个指令在多个cpu上同时执行
多线程的实现方式
继承Thread类的方式实现
- 定义一个类继承Thread
- 重写run方法
- 创建子类的对象,并启动线程
如:
继承Thread的类
public class MyThread extends Thread{
@Override
public void run(){
//要执行的代码
}
}
测试类
public class Test{
public static void main(String[] args){
//创建子类对象
MyThread t1 = new MyThread();
//启动线程
t1.start();
}
}
Thread常用成员方法
| 方法名称 | 说明 |
|---|---|
| String getName() | 返回此线程的名称 |
| void setName(String name) | 设置线程的名字(构造方法也可以设置名字) 如果没有给线程设置名字,默认是:Thread-n(n是序号,从0开始) |
| static Thread currentThread() | 获取当前线程的对象 (当JVM虚拟机启动之后,会自动的启动多条线程其中有一条线程就叫做main线程,作用是去调用main方法,并执行里面的代码,我们写的所有的代码,都是运行在main线程当中) |
| static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
| setPriority(int newPriority) | 设置线程的优先级(1最小,10最大,默认是5,优先级越大抢占到cpu概率越大) |
| final int getPriority() | 获取线程的优先级 |
| final void setDaemon(boolean on) | 设置为守护线程 |
| public static void yield() | 礼让线程 |
| public static void join() | 插队线程 |
守护线程:当其他的非守护线程执行完毕后,守护线程会陆续结束
礼让线程:让当前正在执行的线程,主动让出 CPU 执行权,回到就绪状态,与其他处于就绪状态的线程重新竞争CPU执行权。
插队线程:让当前线程(调用方) 等待” 目标线程执行完毕后,再继续执行,相当于让目标线程 “插队” 到当前线程的前面执行。
实现Runnable接口的方式进行实现
- 定义一个类实现Runnable接口
- 重写里面的run方法
- 创建自己的类的对象
- 创建一个Thread类的对象,并开启线程
如:
实现Runnable的类
public class MyRun implements Runnable{
@Override
public void run(){
//要执行的代码
}
}
测试类
public class Test{
public static void main(String[] args){
//创建子类对象,表示多线程要执行的任务
MyRun run1 = new MyRun();
//创建线程对象,并传入自己定义的实现Runnable的子类的对象
MyThread t1 = new Thread(run1);
//启动线程
t1.start();
}
}
可以用Thread.currentThread()获取当前线程的对象
利用Callable接口和Future接口方式实现
- 创建一个类实现Callable接口
- 重写call(有返回值,表示多线程运行的结果)
- 创建实现Callable接口的类的对象(表示多线程要执行的任务)
- 创建FutureTask的对象(管理多线程运行的结果)
- 创建Thread类的对象并启动
如:
实现Callable的类
public class MyCallable implements Callable<返回值类型>{
@Override
public 返回值类型 call(){
//要执行的代码
}
}
测试类
public class Test{
public static void main(String[] args){
//创建子类对象,表示多线程要执行的任务
MyCallable mc = new MyCallable();
//创建FutureTask对象,并传入自己定义的实现MyCallable的子类的对象
FutureTask<返回值类型> ft = new FutureTask<>(mc);
//创建Thread对象,并传入FutureTask对象
Thread t = new Thread(ft);
//启动线程
t.start();
//获取多线程运行的结果
返回值类型 result = ft.get();
}
}
线程的6种状态
JDK 官方Thread.State枚举的6 个标准状态:
- NEW(新建) :创建线程对象,调用
start()前,线程未启动、未执行任何代码; - RUNNABLE(就绪 / 运行) :调用
start()后,线程就绪等 CPU 执行权 或 正在执行run()方法,就绪和运行合并为该状态; - BLOCKED(阻塞) :争抢
synchronized锁失败,被动等待获取锁,抢到锁即可恢复运行; - WAITING(无限等待) :调用
wait()/无参join()等,主动释放锁永久等待,必须被其他线程唤醒才能恢复; - TIMED_WAITING(计时等待) :调用
sleep()/带参wait()/带参join()等,主动休眠 / 等待,超时自动恢复 或 被提前唤醒; - TERMINATED(终止) :线程
run()执行完毕 或 抛未捕获异常,生命周期永久结束,不可重启。
等待唤醒机制
等待唤醒机制是 Java 中基于 Object 类的 wait()、notify()、notifyAll() 三个方法实现的线程间通信机制,必须配合 synchronized 同步代码块、同步方法使用。
| 特性 | Object.wait() | Thread.sleep(long) |
|---|---|---|
| 所属类 | Object 类(所有对象可调用) | Thread 类(静态方法) |
| 锁资源 | 调用后立即释放锁 | 调用后不释放锁 |
| 线程状态 | RUNNABLE→ WAITING | RUNNABLE→ TIMED_WAITING |
| 使用前提 | 必须在 synchronized 中使用 | 无任何前提,任意位置调用 |
线程池
线程池(ThreadPoolExecutor)是java juc并发包 (java.util.concurrent) 中提供的一种线程复用机制。
本质:一个管理一组预先创建好的线程的容器。线程池会在初始化时创建一批核心线程,线程执行完任务后不会立即销毁,而是回到线程池中成空闲状态,等待下一个任务到来时直接复用。
线程池的优势
- 降低资源消耗
创建线程、销毁线程的开销非常大,线程池通过线程复用,避免了频繁创建、销毁线程的开销,极大降低系统资源消耗。 - 提高响应速度
任务到达时,无需等待线程创建,直接复用线程池中的空闲线程执行任务 - 统一管理线程
可以精准控制并发线程的数量,避免无限制创建线程(比如循环 new 1000 个线程)导致的 CPU、内存耗尽、 - 支持任务扩展
线程池提供了丰富的 API,支持任务排队、任务拒绝、线程超时回收、线程活跃数监控等功能,原生线程做不到。
线程池的工作原理
- 判断 核心 线程池是否已满:若线程池中正在运行的线程数 < 核心线程数,则直接新建核心线程执行任务;
- 核心线程池已满,判断任务队列是否已满:如果核心线程都在工作,则把任务加入阻塞队列排队,核心线程执行完当前任务后,会从队列中取任务继续执行;
- 任务队列已满,判断线程池是否已满:如果队列排满了,新建非核心线程执行任务;
- 线程池已满,触发拒绝策略:如果核心线程 + 非核心线程已达到最大线程数,队列也满了,那么线程池无法处理新任务,执行任务拒绝策略。
线程池的7个核心参数
public ThreadPoolExecutor(
int corePoolSize, // 1.核心线程数
int maximumPoolSize, // 2.最大线程数
long keepAliveTime, // 3.空闲存活时间
TimeUnit unit, // 4.时间单位
BlockingQueue<Runnable> workQueue, // 5.任务阻塞队列
ThreadFactory threadFactory, // 6.线程工厂
RejectedExecutionHandler handler // 7.拒绝策略
)
1. corePoolSize 核心线程数
线程池的常驻线程数量,线程池初始化后默认是空的,当任务来临时才会创建核心线程,核心线程的特点:一旦创建,即使空闲也不会销毁
2. maximumPoolSize 最大线程数
线程池允许创建的最大线程总数 = 核心线程数 + 非核心线程数。
3. keepAliveTime 空闲存活时间 + 4. TimeUnit 时间单位
给非核心线程设置的超时回收规则:当非核心线程空闲的时间超过 keepAliveTime,就会被线程池销毁,释放资源。
- 可选单位:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)、TimeUnit.MINUTES(分钟) 等
5. workQueue 任务阻塞队列
当核心线程全部忙碌时,新任务的排队容器,采用阻塞队列实现
Java 中常用的 3 种队列:
- ArrayBlockingQueue:有界队列,必须指定容量,能有效控制任务堆积;
- LinkedBlockingQueue:无界队列,容量默认Integer.MAX_VALUE,会导致任务无限排队,线程池永远不会创建非核心线程;
- SynchronousQueue:同步队列,无容量,放一个任务必须立刻有线程取走,否则阻塞,适合任务量极大、要求快速执行的场景。
6. threadFactory 线程工厂
用于创建线程的工具类,给线程池创建的线程命名、设置优先级、设置是否为守护线程等。
默认使用 Executors.defaultThreadFactory(),创建的线程名格式为 pool-1-thread-1
7. RejectedExecutionHandler 拒绝策略
线程池的最后兜底方案:当线程数达到最大值 + 任务队列已满时,线程池无法处理新任务,此时就会执行拒绝策略,处理这个无法被执行的任务。
线程池的 4 种默认拒绝策略
1. AbortPolicy
- 行为:直接抛出异常 RejectedExecutionException,中断程序执行;
- 适用场景:生产环境最常用,因为任务无法执行时抛出异常,能及时发现问题,避免静默失败导致的业务异常。
2. CallerRunsPolicy
- 行为:不抛出异常,也不丢弃任务,而是让提交任务的线程自己执行这个任务;
- 适用场景:并发量小、任务重要性一般的场景,能保证任务不丢失,但会阻塞提交线程。
3. DiscardPolicy
- 行为:直接丢弃当前任务,不抛出任何异常
- 适用场景:不推荐,任务丢失了也无法感知,排查问题极其困难。
4. DiscardOldestPolicy
- 行为:丢弃任务队列中排队最久的任务,把当前任务加入队列尝试排队;
- 适用场景:任务有先后顺序、允许丢弃旧任务的场景,比如日志收集、统计上报等非核心任务。
Java 中 4常用的内置线程池
JUC 包提供了工具类 Executors,封装了常用的线程池,底层都是通过ThreadPoolExecutor实现,无需自己手动配置参数。
1. 定长线程池:Executors.newFixedThreadPool (int nThreads)
- 核心参数:
corePoolSize = maximumPoolSize = nThreads,无救急线程,队列是无界的LinkedBlockingQueue - 特点:线程数量固定,全部是核心线程,空闲时不销毁,任务无限排队
//创建固定3个核心线程的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
2. 单线程线程池:Executors.newSingleThreadExecutor ()
- 核心参数:
corePoolSize = maximumPoolSize = 1,只有 1 个核心线程,队列无界 - 特点:串行执行所有任务,保证任务执行顺序,线程挂了会自动新建一个
//只有1个核心线程,任务串行执行
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
3. 缓存线程池:Executors.newCachedThreadPool ()
- 核心参数:
corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,队列是SynchronousQueue(无容量),空闲存活时间 60 秒; - 特点:无核心线程,来任务就创建新线程,线程空闲 60 秒销毁,任务无排队;
//无核心线程,按需创建线程,空闲60秒销毁
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
4. 定时线程池:Executors.newScheduledThreadPool (int corePoolSize)
- 底层是
ScheduledThreadPoolExecutor,继承ThreadPoolExecutor; - 特点:支持延迟执行、定时重复执行任务;
//指定2个核心线程,支持延迟+定时重复执行
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
// 方式1:延迟执行(只执行1次):延迟1秒后执行任务
scheduledThreadPool.schedule(() -> {
System.out.println("定时线程池-延迟任务执行,线程:" + Thread.currentThread().getName());
}, 1, TimeUnit.SECONDS);
// 方式2:固定频率执行(多次):初始延迟2秒,之后每隔3秒执行1次
scheduledThreadPool.scheduleAtFixedRate(() -> {
System.out.println("线程:" + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (Exception e) {} // 任务耗时1秒
}, 2, 3, TimeUnit.SECONDS);
// 方式3:固定延迟执行(多次):初始延迟2秒,上一次执行完后隔3秒再执行
scheduledThreadPool.scheduleWithFixedDelay(() -> {
System.out.println("线程:" + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (Exception e) {} // 任务耗时1秒
}, 2, 3, TimeUnit.SECONDS);
scheduleAtFixedRate和schedulWithtFixedRate区别
- scheduleAtFixedRate(2,3) → 固定频率执行
以上一次任务开始执行的时间计算下一次执行时间
scheduleAtFixedRate(2,3)执行时间线
0秒 程序启动 → 等待初始延迟 2秒
2秒 第一次任务开始执行 → 执行耗时1秒 → 3秒执行完毕
5秒 第二次任务开始执行 → 执行耗时1秒 → 6秒执行完毕
8秒 第三次任务开始执行 → 执行耗时1秒 →9秒执行完毕
11秒 第四次任务开始执行 →...
- scheduleWithFixedDelay(2,3) → 固定延迟执行
以上一次任务执行完毕的时间计算下一次执行时间
scheduleWithFixedDelay(2,3)执行时间线
0秒 程序启动 → 等待初始延迟 2秒
2秒 第一次任务开始执行 → 执行耗时1秒 → 3秒执行完毕 → 开始等3秒
6秒 第二次任务开始执行 → 执行耗时1秒 →7秒执行完毕 → 开始等3秒
10秒 第三次任务开始执行 → 执行耗时1秒 →11秒执行完毕 → ...
线程池的操作
任务提交的两种方式
execute(Runnable task):提交无返回值的任务,无法获取任务执行结果,底层就是线程池的核心执行方法;submit(Callable<T> task):提交有返回值的任务,返回一个Future对象,通过future.get()获取执行结果,适合需要获取任务结果的场景。
线程池的关闭方式
线程池是常驻内存的,程序结束时如果不关闭,线程池的线程会一直存活,导致进程无法退出,内存泄漏。提供 2 种关闭方式,按需选择:
shutdown():温和关闭,不再接收新任务,等待队列中所有任务执行完毕后,再销毁线程;shutdownNow():强制关闭,不再接收新任务,中断正在执行的任务,清空队列,立即销毁线程,适合紧急停止场景。
Lock锁
Lock 是一个 Java 接口,全类名java.util.concurrent.locks.Lock,是一种手动锁:锁的获取、释放都需要开通过代码手动调用方法完成,和synchronized作用一致,都是为了保证多线程下共享资源的安全访问、解决线程安全问题
Lock接口核心API
// 1.获取锁,如果锁被其他线程占用,则一直阻塞等待,直到获取锁成功
void lock();
// 2.释放锁:手动释放锁,解锁成功后其他线程才能获取锁
void unlock();
// 3.尝试获取锁:非阻塞式获取锁
//返回值是boolean:true=获取锁成功 false=获取锁失败(锁被占用)
boolean tryLock();
// 4.超时尝试获取锁:带等待时间的尝试获取锁
// 在指定时间内,能获取锁就返回true;超时还没获取到,返回false;也可以被中断
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 5.获取可中断的锁:获取锁时,如果线程被其他线程中断,则抛出异常,停止等待锁
void lockInterruptibly() throws InterruptedException;
实现类ReentrantLock
Lock 只是接口,不能直接 new 对象,Java 提供了现成的实现类,其中 ReentrantLock 是最常用、最重要、最核心的实现类
- 全类名:
java.util.concurrent.locks.ReentrantLock - Reentrant = 可重入,Lock= 锁
- 核心特点:和 synchronized 一样,都是可重入锁(一个线程可以多次获取同一个锁,不会自己锁死自己)
- 无参构造:Lock lock = new ReentrantLock()
和synchronized区别
| 对比维度 | synchronized(同步锁) | Lock(接口,常用实现类 ReentrantLock) |
|---|---|---|
| 锁的类型 | 隐式锁(JVM 层面的关键字) | 显式锁(Java 代码层面的 API) |
| 加锁 / 释放锁 | 自动加锁、自动释放,无需手动操作线程执行完代码 / 抛异常,JVM 自动解锁 | 手动加锁:lock.lock()手动释放:lock.unlock(),必须写在 finally 中保证释放,否则会锁泄漏 |
| 线程等待是否可中断 | 不可中断,拿不到锁就无限阻塞死等,无法停止等待 | 支持可中断,通过lockInterruptibly()实现,等待锁时可被中断,停止等待 |
| 是否支持尝试获取锁 | 不支持,要么拿到锁执行,要么阻塞等待 | 支持 2 种尝试获取 1.tryLock():无等待,拿不到锁直接返回 false 2.tryLock(时间,单位):超时拿不到锁返回 false |
| 公平锁 / 非公平锁 | 只支持非公平锁 | 都支持,创建时指定 非公平锁(默认): new ReentrantLock()公平锁: new ReentrantLock(true)(按排队顺序拿锁) |
| 线程唤醒方式 | 只能配合 wait()/notify()/notifyAll()随机唤醒一个线程 或 唤醒全部线程,无法精准唤醒指定线程 | 支持Condition接口可以绑定多个条件队列,精准唤醒指定线程,不唤醒其他线程 |
| 性能 | JDK1.6 后 JVM 做了大量优化,性能和 Lock 持平 | JDK1.5 性能远超 synchronized,JDK1.6 后两者性能基本一致 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)