初识多线程(3.0)

notify()⽅法

notify ⽅法 是 唤醒等待的线程.

此外要注意

这两个要锁的是同一个对象

否则将不会产生任何影响

notifyAll

notify⽅法只是唤醒某⼀个等待线程.使⽤notifyAll⽅法 可以⼀次唤醒所有的等待线程.

虽然从结果是同时唤醒 t1 t2

但是实则不然

wait唤醒后重新加锁

为其中一个线程加锁 另一线程阻塞等待

要等待先前 的先执行完成后 后者才能加锁

如果只调用某一个线程效果如下:

当多个线程 在同一对象使用wait

notify会随机调度一个线程的

wait 和sleep的对⽐

1.wait 需要搭配 synchronized 使⽤. sleep 不需要.

)如果都在synchronized内使用

那sleep 将不会释放锁(抱锁💤)

其他线程无法得到这个锁

2.waitObject 的⽅法sleepThread的静态⽅法.

wait 可使用 notify 提前唤醒

sleep 可使用 Interrput 提前唤醒

单例模式

单例模式 是最常见设计模式之⼀

(简单地说 在面对软件开发中所出现的各种问题 先人总结了一些方法 按照这个套路来实现代码)

单例模式能保证某个类在程序中只存在唯⼀⼀份实例,⽽ 不会创建出多个实例.

单例模式具体的实现⽅式有很多.最常⻅的是**"饿汉"** 和 "懒汉" 两种

饿汉模式

复制代码
Singleton

类加载的同时,创建实例.

懒汉模式

SingletonLazy

懒汉方式却恰恰相反

(遵循着非必要不创建)

复制代码

但在多线程中 饿汉 和 懒汉 又有什么不一样的吗?

饿汉

在饿汉模式 String 是 天然安全的 不涉及 到线程安全

懒汉

但在 多线程中 上述实现是线程不安全的.

线程安全问题发⽣在⾸次创建实例时.

如果在多个线程中同时调⽤getInstance⽅法,就可能导致创建 出多个实例.

所以在处理线程安全中的常规手段 要加上 synchronized 可以改善这⾥的线程安全问题.

修改是原子的 操作

所以修改是要把 要修改的对象打包成 原子性的

但是在加锁前也是要确认 当前的对象是否需要加锁

加锁/解锁是⼀件开销⽐较⾼的事情.⽽懒汉模式的线程不安全只是发⽣在⾸次创建实例的时候.因此 后续使⽤的时候,不必再进⾏加锁了.

在多线程懒汉模式 还有可能引起 **"内存可见性"**的问题

当 T1 线程 在 读写时 T2 线程修改

复制代码
private static volatile SingletonLazy instance = null;

加入volatile 1.确保从内存中完成读取操作

2.关于该变量的修改不会涉及到读取和修改操作 ,不会触发重排序

编译器会调整 代码执行顺序 指令重排序

在编译逻辑不变的前提之下 将代码的执行顺序改变以此来优化执行性能和效率

此外上述图中还涉及到了一点 双重if的使用

外层的if就是判定下看当前是否已经把instance实例创建出来了

多线程⾸次调⽤getInstance, 其他线程可能都发现instancenull,

于是⼜继续往下执⾏来竞争锁,其 中竞争成功的线程,再完成创建实例的操作.

创建实例后 其他竞争到锁的线程就被 ⾥层if 挡住了.也就不会继续创建其他实例

阻塞队列

是一种特殊的队列

1.线程安全

2.阻塞队列特性 遵守"先进先出"的原则.

当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素

当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插⼊元素.

⽣产者消费者模型

阻塞队列的⼀个典型应⽤场景就是**"⽣产者消费者模型"**

⽣产者消费者模型 有着两个重要的优势

1.解耦合

不仅存在于 两个线程中 也可以是服务器中

2.消峰填谷

将服务器的请求量看似为一个曲线图

在Java标准库中内置了阻塞队列.

如果我们需要在⼀些程序中使⽤阻塞队列,直接使⽤标准库中的即可

BlockingQueue 接⼝. 底层实现由 LinkedBlockingQueue类完成

put⽅法⽤于阻塞式的⼊队列,

take ⽤于阻塞式的出队列.

BlockingQueue 也提供了其他方法(offer,poll,peek) 但是其中不含阻塞特性.

实现

实现一个简单的生产者 消费者模型

首先自己定义一个简单的阻塞队列 MyBlockingQueue

内部要实现 put 和 take (队列 的先进先出)

定义基础变量 再完成put take 的实现

put
take

其中 notify 是put take 相互唤醒的

但是存在 多个线程put 多个线程take 可能会触发自我调用

线程池

线程池是一种多线程处理形式,通过预先创建 一组线程并管理它们的生命周期,优化系统资源的使用

本质一组线程

主要用途:高效创建 销毁 线程

为了高效创建 销毁 线程 目前保留着两种主流的方法

1.线程池

2. 协程(一种比线程更轻量级的并发编程模型

直接创建线程 会比从线程池中取的开销会更大

简单说 直接创建线程 需要 操作系统(内核 + 配套程序)

从内核中完成的 操作系统创建线程 这一状态是 不可控

但是从线程池取出 线程 这一状态是 可控

所以导致了线程池省下一些开销

标准库中的线程池

Executors 本质上是 ThreadPoolExecutor类的封装.

ThreadPoolExecutor

corePoolSize(核心线程数)

类比于 公司正式员工数量.

maximumPoolSize(最大线程数)

正式员工 + 实习生 的数量

keepAliveTime(非核心线程允许最大空闲时间)

实习生允许的 空闲**(摸鱼)**时间

unit( 是keepaliveTime 的时间单位),

workQueue(传递任务的阻塞队列 工作队列)

threadFactory(工厂模式)

和单例模式 并列的关系

给线程类提供的 工厂类

对⼀些属性进⾏了不同的初始化设置.

工厂方法 的核心是 把构造对象new 的类过程

各种属性初始化的过程 封装起来

提供 工厂方法 的类就是工厂模式

RejectedExecutionHandler(拒绝策略)

如果任务量超出公司的负荷了接下来怎么处理

一般有下列的 四种 处理方法

AbortPolicy(): 超过负荷,直接抛出异常.

CallerRunsPolicy(): 让调⽤sumbit负责处理多出来的任务.

submit,将任务加⼊线程池中

DiscardOldestPolicy(): 丢弃队列中最⽼的任务.

DiscardPolicy(): 丢弃新来的任务.

定时器

标准库中提供了⼀个Timer类

Timer类的核⼼⽅法为 schedule

定时器也是软件开发中的⼀个重要组件.

类似于⼀个"闹钟".达到指定时间之后,就执⾏某个指定好的代码

实现

MyTimerTask

Task类⽤于描述⼀个任务(作为Timer的内部类)

⾥⾯包含⼀个Runnable对象和⼀个time(毫秒时间戳)

所以这个对象需要放到 优先队列**(** 确保在多任务时 最先执行最快完成**)**中.因此需要实现 Comparable接⼝

Mytimer

Timer类提供的核⼼接⼝为schedule

⽤于注册⼀个任务,并指定这个任务多⻓时间后执⾏.

通过PriorityQueue来组织若⼲个Task对象

通过schedule来往队列中插⼊⼀个个Task对象.

Timer类 中不停的扫描队⾸元素,判断是否能执⾏这个任务

定时器除了使用有优先级队列实现 也可以使用 '时间轮'实现

优点:性能更高

缺点:时间精度不如 优先级队列

相关推荐
小杍随笔10 小时前
【Rust 工具链管理工具再升级!rust-verse v1.3.1 ~ v1.3.5 最新更新深度解析】
开发语言·后端·rust
北漂人Java11 小时前
SpringAI-2.Spring AI整合本地模型和云端大模型
java·spring
迹象Kimizhou_blog11 小时前
国内 IntelliJ IDEA 集成Claude code,调用deepSeek模型实现agent
java·ide·intellij-idea·deepseek·claude code
大数据三康11 小时前
在spyder进行的遗传算法练习
开发语言·python·算法
百珏11 小时前
海量人群包存储优化:基于 RoaringBitmap 交换格式与 Redis 分片 Bitmap 的实践
java·后端·架构
风味蘑菇干11 小时前
IO流(字节流)
java
Vallelonga11 小时前
Rust 从结构体中取字段的引用
开发语言·rust
社交怪人11 小时前
【球体体积】信息学奥赛一本通C语言解法(题号1030)
c语言·开发语言
froginwe1111 小时前
Foundation 顶部导航栏详解
开发语言