当前位置: 首页 > news >正文

线程池、锁策略

目录

线程池

模拟实现线程池

定时器

实现定时器

时间轮

常见的锁策略

乐观锁 vs 悲观锁

重量级锁 vs 轻量级锁

挂起等待锁 vs 自旋锁

普通互斥锁 vs 读写锁

可重入锁 vs 不可重入锁

公平锁 vs 非公平锁

总结

synchronized 原理

基本特点

加锁工作过程

​编辑偏向锁


线程池

上古时期,服务器处理多个客户端的请求,是基于多进程的模型。每次有个客户端请求过来了,服务器这边都创建一个进程给这个客户端提供服务(读取请求,解析请求,返回响应…….)。后来,频繁创建销毁进程,效率比较低,所以引入了线程

引入线程后,模型就成了,每个客户端都分配一个线程提供服务。随着客户端数量越来越多,发现频繁创建销毁线程,也变的比较低效了,又引入了线程池

线程池最主要的还是解决服务器开发的问题,假设线程池里提前准备好10个线程,有100个客户端吧请求发过来,把这100个客户端的请求封装成任务(Runnable)添加到线程池里,线程池中由这10个线程负责处理这100个任务,这个过程就不涉及线程的创建销毁了

模拟实现线程池

1. 核心操作为 submit,将任务加入线程池中,任务就是 runnable

2. 使用 Worker 类描述一个工作线程,使用 Runnable 描述一个任务

3. 使用一个 BlockingQueue 组织所有的任务

4. 每个 worker 线程要做的事情:不停的从 BlockingQueue 中取任务并执行

5. 指定一下线程池中的最大线程数 maxWorkerCount;当当前线程数超过这个最大值时,就不再新增线程了

// 实现一个固定线程个数的线程池
class MyThreadPool {private BlockingQueue<Runnable> queue = null;public MyThreadPool(int n) {// 初始化线程池,创建固定个数的线程// 这里使用ArrayBlockingQueue作为任务队列, 容量为1000queue = new ArrayBlockingQueue<>(1000);// 创建 N 个线程for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {try {while (true) {Runnable task = queue.take();task.run();}} catch (InterruptedException e) {e.printStackTrace();}});// t.setDaemon(true); // 设置为后台线程t.start();}}public void submit(Runnable task) throws InterruptedException {// 将任务放入队列中queue.put(task);}
}public class Demo35 {public static void main(String[] args) throws InterruptedException {MyThreadPool pool = new MyThreadPool(10);// 向线程池提交任务for (int i = 0; i < 100; i++) {int id = i;pool.submit(() -> {System.out.println(Thread.currentThread().getName() + " id=" + id);});}}
}

虽然100个任务已经结束了但是进程仍然没有结束,此时线程池里的这些线程,还在 take 阻塞,说明线程池中的线程,是前台线程,会阻止进程结束!

补充知识:

手动停止上述程序,显示的是数字 130,这个数字表示程序的退出码

操作系统中约定了:退出码为 0,表示正常结束;非0表示异常结束(不同的数字表示不同的原因)

正常结束:

定时器

定时器是软件开发中的一个重要组件,类似于一个 “闹钟”,达到一个设定的时间之后,就执行某个指定好的代码

定时器是一种实际开发中非常常用的组件。比如网络通信中,如果对方 500ms 内没有返回数据,则断开连接尝试重连。比如一个Map,希望里面的某个 key 在 3s 之后过期(自动删除)。类似这样的场景就需要用到定时器

标准库中的定时器

  • 标准库中提供了一个 Timer 类,Timer 类的核心方法为 schedule
  • schedule 包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)
public class Demo37 {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello 3000");}}, 3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello 2000");}}, 2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello 1000");}}, 1000);System.out.println("hello main");}
}

1. TimerTask 实现了 Runnable,核心还是重写 run 方法

2. 和线程池一样,Timer中也包含前台线程,会阻止进程结束

实现定时器

1. 创建一个类,表示一个任务

2. 定时器中,要能够管理多个任务,必须使用一些集合类把这多个任务给管理起来。这里使用优先级队列(不要使用 PriorityBlockingQueue,容易死锁)

ArrayList 是否可行呢?

不可行。当管理多个任务的时候,需要确保,时间最早的任务最先执行,ArrayList 要通过遍历的方式,找到时间最早的任务,指定的时间可能不准

3. 实现 schedule 方法。把任务添加到队列中即可

4. 额外创建一个线程,负责执行队列中的任务。和线程池不同,线程池是只要队列不为空,就立即取任务并执行。这里需要看队首元素的时间是否到了,时间到才能执行,时间不到不能执行

// 基于抽象类的方式定义 MyTimerTask 的写法
// 这样的定义虽然确实可以, 但写起来有点麻烦,还有另外的写法
//abstract class MyTimerTask implements Runnable {
//    @Override
//    public abstract void run();
//}class MyTimerTask implements Comparable<MyTimerTask> {private Runnable task;// 记录任务要执行的时刻private long time;public MyTimerTask(Runnable task, long time) {this.task = task;this.time = time;}@Overridepublic int compareTo(MyTimerTask o) {return (int) (this.time - o.time);// return (int) (o.time - this.time);}public long getTime() {return time;}public void run() {task.run();}
}// 自己实现一个定时器
class MyTimer {private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 直接使用 this 作为锁对象, 当然也是 ok 的private Object locker = new Object();public void schedule(Runnable task, long delay) {synchronized (locker) {// 以入队列这个时刻作为时间基准.MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);queue.offer(timerTask);locker.notify();}}public MyTimer() {// 创建一个线程, 负责执行队列中的任务Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {// 取出队首元素// 这里使用 whilewhile (queue.isEmpty()) {locker.wait();}MyTimerTask task = queue.peek();if (System.currentTimeMillis() < task.getTime()) {// 当前任务时间, 如果比系统时间大, 说明任务执行的时机未到locker.wait(task.getTime() - System.currentTimeMillis());} else {// 时间到了, 执行任务task.run();queue.poll();}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}
}public class Demo38 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 3000");}}, 3000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("hello 1000");}}, 1000);// 带有线程池的计时器Executors.newScheduledThreadPool(4);}
}

说明:

1. 优先级队列必须要明确的比较规则。如果元素是 数字 或 String 这种本身就有明确比较规则的对象,可以不额外指定

2. 调用schedule是一个线程,定时器内部又有一个线程,多个线程操作同一个队列,一定涉及到线程安全问题,需要使用锁

3. 锁加到下图这个位置,可能会因为 synchronized 阻塞,使得时间存在一点误差,所以建议把 timeTask 也放到锁里

4. 标准库提供的 Timer 和自己实现的 MyTimer 差不多,都是使用一个线程,负责扫描队首元素并执行的。如果任务少或任务的时间分散问题不大,如果任务特别多,时间非常集中一个线程就可能执行不过来。更好的做法是 一个线程负责扫描,扫描到需要执行的任务,添加到另一个线程池的任务队列中,由多个线程负责执行

5. 标准库中带有线程池的计时器:Executors.newScheduledThreadPool();

时间轮

除了基于堆(优先级队列)的方式来实现的定时器之外,还有一种方案,基于 “时间轮”

时间轮类似循环队列(数组),每个元素是一个 “时间单位”,每个元素又是一个链表。每到一个时间单位,光标指向下一个元素,同时把这个元素上对应链表中的任务都执行一遍

优势:性能更高(优先级队列有堆调整的开销)

劣势:时间精度不如优先级队列

时间轮更适合任务特别多的情况

优先级队列更适合精度高的情况

由于定时器,是一个非常重要的组件,以至于在分布式系统中,会把定时器专门提取出来,封装成一个单独的服务器(和消息队列很像)


常见的锁策略

如果你认为标准库提供的锁不够用,想自己实现一把锁,这时需要关注锁策略(其实 synchronized 已经非常好用了,足矣覆盖绝大多数的使用场景)

“锁策略” 不是和Java强相关的。其他语言,但凡涉及到并发编程,涉及到锁,都可以谈到锁策略

锁策略,就是锁在加锁时特点和行为

乐观锁 vs 悲观锁

描述的是加锁时遇到的场景

悲观锁:加锁的时候,预测接下来的锁竞争的情况非常激烈,就需要针对这样的激烈情况额外做一些工作

有一把锁,有二十个线程尝试获取锁,每个线程加锁的频率都很高。一个线程加锁的时候,很可能锁被另一个线程占用着。这种情况用悲观锁的方式处理

乐观锁:加锁的时候,预测接下来的锁竞争的情况不激烈,不需要做额外工作

有一把锁,假设只有两个线程尝试获取这个锁。每个线程加锁的频率都很低,一个线程加锁的时候,大概率另一个线程没有和他竞争。这种情况用乐观锁的方式处理

重量级锁 vs 轻量级锁

遇到场景之后的解决方案

重量级锁,在悲观的场景下,就要付出更多的代价(更低效)

轻量级锁,应对乐观的场景,付出的代价就会更小(更高效)

挂起等待锁 vs 自旋锁

自旋锁:轻量级锁的典型实现

是应用程序级别的。加锁的时候发现有竞争,一般不是进入阻塞,而是通过忙等的形式进行等待

忙等:连续测试一个变量直到某个值出现为止,称为忙等。忙了很长时间,又没有实质性的产出,还持续占着 CPU 而不释放,这个时候应该释放 CPU 让给其他进程

^

在乐观锁的场景下,遇到锁竞争的概率很小,就算遇到竞争,也能在短时间内就能拿到锁,获取锁的周期很短,所以使用忙等的方式影响不大

挂起等待锁:重量级锁的典型实现

是操作系统内核级别的。加锁的时候发现有竞争,就会使该线程进入阻塞状态,后续由内核进行唤醒

获取锁的周期更长,很难做到及时获取,所以这个过程就不必使用忙等的方式一直消耗 cpu 了,把 cpu 省出来做别的事情

synchronized 是悲观 / 乐观呢?

答:既是悲观又是乐观,可以自适应。

VM内部会统计每个锁竞争的激烈程度,如果竞争不激烈,synchronized 就会按照轻量级锁(自旋),如果竞争激烈,synchronized 就会按照重量级锁(挂起等待)

普通互斥锁 vs 读写锁

普通互斥锁:synchronized;只有加锁、解锁两个操作

读写锁:有两种加锁方式:读方式加锁、写方式加锁

1. 多个线程读取一个数据,本身就是线程安全的。多个线程读取,一个线程修改,肯定会涉及到线程安全问题。如果大部分操作在读,少数操作在写,把读和写都加普通的互斥锁,锁冲突非常严重,所以就引入了读写锁

2. 读写锁可以确保,读锁和读锁之间不是互斥的(不会产生阻塞),写锁和读锁之间,才会产生互斥,写锁和写锁之间,也有互斥。

3. 读写锁保证线程安全的前提下,降低锁冲突的概率,提高效率

4. 读写锁适合 读多,写少 的情况,这种情况在服务器开发中很常见

Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁

这两个内部类各自提供了 lock / unlock 方法进行加锁解锁

可重入锁 vs 不可重入锁

一个线程,一把锁,连续加锁多次,是否会死锁,死锁了就是不可重入,没有死锁就是可重入

synchronized是 “可重入锁”

核心要点:

1. 在锁中记录当前是哪个线程拿到的这把锁

2. 使用计数器,记录当前加锁了多少次,在合适的时候进行解锁

公平锁 vs 非公平锁

假设三个线程 A,B,C。A 先尝试获取锁,获取成功,然后B再尝试获取锁,获取失败,阻塞等待;然后 C 也尝试获取锁,C也获取失败,也阻塞等待。当线程 A 释放锁的时候,B 和 C 谁先拿到锁呢?

公平锁:遵守 “先来后到”,B 比 C 先来的,当 A 释放锁的之后,B 就能先于 C 获取到锁
非公平锁:不遵守 “先来后到” B 和 C 都有可能获取到锁(synchronized 就是非公平锁)

要想实现公平锁,需要付出额外的代价,比如,需要使用一个队列,记录一下各个线程获取锁的顺序

总结

1. synchronized 在上述策略中,是什么样的锁?

是自适应的,乐观锁、悲观锁、重量级锁、轻量级锁、挂起等待锁、自旋锁,这些都可以是;

不是读写锁;是可重入锁,非公平锁

2. 介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁

  • 读锁和读锁之间不互斥
  • 写锁和写锁之间互斥
  • 写锁和读锁之间互斥

读写锁最主要用在“频繁读,不频繁写”的场景中

3. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止。第一次获取锁失败,第二次的尝试会在极短的时间内到来,一旦锁被其他线程释放,就能第一时间获取到锁

相比于挂起等待锁,

优点:没有放弃CPU资源,一旦锁被释放就能第一时间获取到锁,更高效;在锁持有时间比较短的场景下非常有效

缺点:如果锁的持有时间较长,就会浪费CPU资源

4. synchronized 是可重入锁么?

是可重入锁

可重入锁是指连续两次加锁不会导致死锁.

实现的方式是

1)在锁中记录当前是哪个线程拿到的这把锁

2)使用计数器,记录当前加锁了多少次,在合适的时候进行解锁

synchronized 原理

基本特点

结合上面的锁策略,可以总结出 synchronized 具有以下特性(JDK1.8):

1. 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁

2. 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁

3. 实现轻量级锁时,大概率用的是自旋锁策略

4.是一种不公平锁

5. 是一种可重入锁

6. 不是读写锁

加锁工作过程

JVM 将 synchronized 锁分为无锁、偏向锁、轻量级锁、重量级锁状态。会根据情况,依次进行升级


偏向锁

1)加锁时,刚一上来不是真加锁,而是只是简单做一个标记,这个标记非常轻量,相比于加锁解锁来说,效率高很多。

2)如果没有其他线程来竞争这个锁,最终当前线程执行到解锁代码,也就只是简单清除上述标记即可(不涉及真加锁,真解锁)

3)如果有其他线程来竞争,会抢先一步,在另一个线程拿到锁之前,抢先拿到锁,此时才是真正加上锁(其他线程只能阻塞等待),偏向锁就升级成轻量级锁了

4)偏向锁也是懒汉模式思想的体现

锁升级的时机/条件:

无锁 => 偏向锁:代码进入 synchronized 的代码块

偏向锁 => 轻量级锁:拿到偏向锁的线程运行过程中,遇到了其他线程尝试竞争这个锁

轻量级锁 => 重量级锁:JVM发现,当前竞争锁的情况非常激烈

JVM 中,只能 “锁升级” 不能 “锁降级” 。可能是因为,实现降级的收益不大,并且会使 JVM 相关代码的复杂度更高

http://www.dtcms.com/a/359360.html

相关文章:

  • Qt中UDP回显服务器和客户端
  • 第三十二天:数组
  • 如何保证redis和mysql的数据一致性
  • Spring Boot 3.x 微服务架构实战指南
  • 基于单片机停车场管理系统/车位管理/智慧停车系统
  • 大模型——xAI 发布 Grok Code Fast 1 编程模型,快、便宜、免费
  • 华为研发投资与管理实践(IPD)读书笔记
  • 第六章:透明度-Transparency《Unity Shaders and Effets Cookbook》
  • 机器视觉学习-day14-绘制图像轮廓
  • 基于Spring Cloud Sleuth与Zipkin的分布式链路追踪实战指南
  • 《深入剖析Kafka分布式消息队列架构奥秘》之Springboot集成Kafka
  • 【重学MySQL】九十四、MySQL请求到响应过程中字符集的变化
  • html添加水印
  • 馈电油耗讲解
  • 特殊符号在Html中的代码及常用标签格式的记录
  • Spring Task快速上手
  • 【多模态】使用LLM生成html图表
  • 【 复习SpringBoot 核心内容 | 配置优先级、Bean 管理与底层原理(起步依赖 + 自动配置) 】
  • 堆排序:高效稳定的大数据排序法
  • Kubernetes 服务发现与健康检查详解
  • 解锁GPU计算潜能:深入浅出CUDA架构与编程模型
  • ESP32学习笔记_Peripherals(5)——SPI主机通信
  • Asible——将文件部署到受管主机和管理复杂的Play和Playbook
  • 局域网中使用Nginx部署https前端和后端
  • Idea启动错误-java.lang.OutOfMemoryError:内存不足错误。
  • Polkadot - ELVES
  • 鸿蒙搭配前端开发:应用端与WEB端交互
  • SCARA 机器人工具标定方法
  • 【算法笔记】算法归纳整理
  • 从零开始的python学习——语句