文章目录
- 一、单例模式
-
- [1. 什么是单例模式](#1. 什么是单例模式)
- [2. 饿汉模式](#2. 饿汉模式)
- [3. 懒汉模式](#3. 懒汉模式)
- [4. 懒汉模式的线程安全问题](#4. 懒汉模式的线程安全问题)
- 二、阻塞队列
- 三、线程池
-
- [1. 什么是线程池](#1. 什么是线程池)
- [2. Java标准库线程池:ThreadPoolExecutor](#2. Java标准库线程池:ThreadPoolExecutor)
-
- 核心参数
-
- [ThreadFactory threadFactory线程工厂](#ThreadFactory threadFactory线程工厂)
- [RejectedExecutionHandler handler 拒绝策略](#RejectedExecutionHandler handler 拒绝策略)
- [3. Executors:快速创建线程池](#3. Executors:快速创建线程池)
- [4. 手搓简易线程池](#4. 手搓简易线程池)
- 四、定时器
-
- [1. 什么是定时器](#1. 什么是定时器)
- [2. Java标准库定时器:Timer类](#2. Java标准库定时器:Timer类)
- [3. 手搓简易定时器](#3. 手搓简易定时器)
一、单例模式
1. 什么是单例模式
设计模式是软件开发里通用、成熟、写好的代码套路,是前辈程序员总结出来解决常见编程问题的最优写法,用来规范代码、好维护、好复用、少踩坑。
单例模式是经典设计模式之一,保证某个类在整个程序中只有唯一一份实例,避免重复创建实例浪费资源,同时确保多线程下实例访问的一致性。
常见应用场景:数据库连接池,配置文件读取类,日志工具类。
单例模式可以通过饿汉模式和懒汉模式来实现。
2. 饿汉模式
类加载时直接初始化静态实例,类加载过程由JVM保证线程安全,天然避免多线程安全问题
java
class Singleton{
public static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){}
}

"饿汉" 表示一种急迫感,在程序一启动,类一加载就立即实例化。
- 缺陷: 提前占用内存,即使实例未被使用也会创建。
3. 懒汉模式
"懒汉"表示延迟,尽可能的晚创建实例,不用就不创建
计算机中"懒"是当之无愧的褒义词。"懒"的另一层含义是效率高
实例在第一次调用getInstance()时创建,实现延迟加载。
java
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance==null)
instance = new SingletonLazy();
return instance;
}
//私有构造方法
private SingletonLazy(){};
}

4. 懒汉模式的线程安全问题
对于饿汉模式,只有return,是读操作,相对来说线程安全;而懒汉模式涉及到写操作,就有很大的隐患。

这段代码逻辑中包括判断和创建实例两个操作,假设有两个线程并发,执行循序如下:

逻辑判断都是真,创建了两个实例。虽然当第二个实例创建出来,第一个实例就会被GC释放掉,但是创建对象的过程可能有极大的消耗,单例模式的主要作用就是防止多余的实例被创建消耗资源,因此要避免这种行为。
以下是线程安全的懒汉模式:
synchronized
加synchronized锁,保证同一时间只有一个线程进入创建逻辑
java
class SingletonLazy{
private static SingletonLazy instance = null;
private static Object locker = new Object();
public static SingletonLazy getInstance(){
synchronized (locker){
if(instance==null)
instance = new SingletonLazy();
return instance;
}
}
//私有构造方法
private SingletonLazy(){};
}
-
缺陷: 创建实例后,每次调用getInstance方法,就不会进入创建逻辑,只是
判断+return,是纯粹的读操作,就不涉及线程安全了。但是每次调用都会加锁,会相互阻塞,大大降低效率。 -
解决方案: 如果实例已经创建就不涉及线程安全,未创建就涉及线程安全。只要按需加锁,创建时加锁,不创建就不加锁。
java
public static SingletonLazy getInstance(){
if(instance == null){
synchronized (locker){
if(instance==null)
instance = new SingletonLazy();
}
}
return instance;
}
这段代码连着写了两个相同的if语句,看似诡异,其实这正是与单线程代码不同之处。
- 单线程代码中,连续两个相同的if语句是无意义的,因为执行流只有一个,判断的结果也一定是一样的
- 多线程代码中,多个执行流并行。在这两个if判断之间,很有可能有其他线程把
instance的值改变,导致两个if判断结果不同。这两个if的作用也不相同。

volatile
instance = new LazySingleton()分为三步:
- 分配内存;
- 初始化实例;
- 赋值给instance。
JVM可能指令重排序为1→3→2,导致某线程获取到未初始化完成的半实例,volatile可禁止重排序,保证线程安全。
java
public class LazySingleton {
// volatile:禁止指令重排序,保证多线程可见性
private static volatile LazySingleton instance = null;
public static LazySingleton getInstance() {
// 第一层判断:实例已创建,直接返回,避免加锁
if (instance == null) {
// 保证同一时间只有一个线程进入
synchronized (locker){
// 第二层判断:防止多线程同时进入第一层判断后,重复创建实例
if(instance==null)
instance = new SingletonLazy();
}
}
return instance;
}
}
二、阻塞队列
1. 什么是阻塞队列
阻塞队列(BlockingQueue)是线程安全的队列,具备两个阻塞特性:
- 队列空时:尝试出队列,出队列操作会阻塞等待,直到队列有新元素。
- 队列满时:尝试入队列,入队列操作会阻塞等待,直到队列有空闲位置;
2. Java标准库阻塞队列:BlockingQueue接口
常用实现类:
LinkedBlockingQueue:无界阻塞队列,默认容量Integer.MAX_VALUE,生产快、消费慢时可能内存溢出;ArrayBlockingQueue:有界阻塞队列,需指定容量,固定内存,生产速度可控;PriorityBlockingQueue:优先级阻塞队列,按元素优先级排序。
核心方法:
| 方法 | 作用 | 阻塞特性 |
|---|---|---|
put(E e) |
入队 | 队列满则阻塞 |
take() |
出队 | 队列空则阻塞 |
offer(E e) |
入队 | 队列满返回false,不阻塞 |
poll() |
出队 | 队列空返回null,不阻塞 |
创建阻塞队列最设置容量,否则队列可能变得非常大,导致把内存耗尽,产生内存超出异常这样的异常
3. 生产者-消费者模型
优势
- 解耦合 (两个线程或两个服务器之间)

如果A直接访问B,A的代码中就会涉及B,A和B的耦合就更高。

如果在A和B之间加入阻塞队列,A和队列交互,B也和队列交互,A和B不再直接交互,A和B就实现了解耦合。
消息队列的功能非常重要,多数情况会把队列单独部署成一个服务,独立的服务的阻塞队列,称为消息队列
队列的功能比较固定,不涉及大规模改动,所有不需要担心A与队列以及B与队列增加耦合的情况
- 削峰填谷
- A这种上游的服务器,尤其入口服务器,单个请求消耗的资源少;
- B这种下游服务器,承担更重的任务量,包括复杂的计算和存储工作,单个请求消耗的资源更多。
- 对于队列服务器,针对单个请求,只需要做存储和转发工作,可以承受很高的请求量。
这样利用生产者-消费者模型,B不需要关心队列中的数据量有多少,只需要按照自己的节奏依次处理请求即可。
劣势
- 引入队列后,需要更多的机器进行部署,生产环境的结构更复杂,管理起来更麻烦
- 对比服务器直接通信,引入队列效率有一定影响
代码实现
- 需求:生产者线程生产n ,放入阻塞队列;消费者线程从队列中取数并打印,实现线程间安全协作。
java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class demo24 {
public static void main(String[] args) {
//生产者,消费者各一个线程
BlockingQueue<Integer> queue = new LinkedBlockingDeque<>(1000);
Thread producer = new Thread(()->{
int n = 0;
while(true){
try {
queue.put(n);
System.out.println("生产元素:"+n);
n++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"producer");
Thread consumer = new Thread(()->{
try {
while(true){
Integer n = queue.take();
System.out.println("消费元素:"+n);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"consumer");
producer.start();
consumer.start();
}
}
4. 手搓简易阻塞队列
基于wait()和notifyAll()实现,核心逻辑:
put():队列满则wait(),不满则入队,唤醒消费者;take():队列空则wait(),不空则出队,唤醒生产者。
java
class MyBlockingQueue{
private String[] data;
private int head;
private int tail;
private int size;
public MyBlockingQueue(int capacity){
data = new String[capacity];
}
public void put(String e) throws InterruptedException {
synchronized (this){
if(size>=data.length){
//队列满了,阻塞
//当执行了take(),说明队列有空余,就可以唤醒wait
this.wait();
}
data[tail++] = e;
if(tail==data.length)
tail = 0;
size++;
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this){
if(size==0){
//阻塞
this.wait();
}
String ret = data[head];
head++;
if(head== data.length)
head = 0;
size--;
this.notify();
return ret;
}
}
}
以上的代码还存在漏洞
这段代码的逻辑是:当take()成功执行,说明队列不满,就唤醒put()里的wait(),进行入队操作。
但是除了notify(),线程也可能在没有超时、没有被中断的情况下,自己从wait() 醒来。这是JVM / 操作系统线程调度层面允许的行为,不是代码 Bug。这就是虚假唤醒 ,此时没有take()成功执行,如果让wait()以后的代码执行,逻辑就乱套了。
因此需要把if改成while,多次判断这个条件是否成立。
- 修正版
java
class MyBlockingQueue{
private String[] data;
private int head;
private int tail;
private int size;
public MyBlockingQueue(int capacity){
data = new String[capacity];
}
public void put(String e) throws InterruptedException {
synchronized (this){
while(size>=data.length){
//队列满了,阻塞
//当执行了take(),说明队列有空余,就可以唤醒wait
this.wait();
}
data[tail++] = e;
if(tail==data.length)
tail = 0;
size++;
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this){
while(size==0){
//阻塞
this.wait();
}
String ret = data[head];
head++;
if(head== data.length)
head = 0;
size--;
this.notify();
return ret;
}
}
}
三、线程池
1. 什么是线程池
上古时期,服务器基于多进程模型来处理多个客户端的请求,对于每一个客户端请求,服务器都要创建一个进程来给客户端提供服务,包括读取请求,解析请求,返回响应...
随着客户端访问量增多,频繁创建和销毁进程开销大,效率低,于是多线程模型应运而生,服务器为每一个客服端请求分配一个线程,提供服务。
随着客户端数量增多,频繁的创建和销毁线程也变得低效,因此引入了线程池。
线程池是线程复用管理工具 ,核心思想:提前创建一批线程,放入池中;任务到来时,直接分配空闲线程执行;任务完成后,线程回归池,等待下一次任务。省略的手动start()的过程。
解决痛点:
- 频繁
new Thread()创建、销毁线程,开销大、效率低; - 无限制创建线程,导致内存溢出、系统崩溃;
- 线程统一管理、监控,可控制并发数。
2. Java标准库线程池:ThreadPoolExecutor
创建线程池后,通过submit()方法向线程池提交任务,任务通常以Runnable接口实现类的形式存在。
java
ExecutorService pool = Executors.newFixedThreadPool(10);
// 提交普通任务
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行任务");
}
});
核心参数
java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数:长期存活,不主动销毁
int maximumPoolSize, // 最大线程数:核心+临时线程总数上限
long keepAliveTime, // 临时线程空闲存活时间:超时销毁
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列:存放等待执行的任务
ThreadFactory threadFactory, // 线程工厂:创建线程,可自定义线程名
RejectedExecutionHandler handler // 拒绝策略:任务超负载时的处理方式
)
corePoolSize:核心线程数- Java的线程池里面包含多个线程,可以动态调整,任务多的时候,自动扩容线程池;任务少的时候,把额外的线程干掉节省资源。
- 核心线程数可以理解为线程池中最少的线程数,线程池一创建,这些线程就会随之创建。直到线程池销毁,这些线程才会销毁
maximumPoolSize最大线程数,核心线程数+非核心线程数- 非核心线程在不繁忙时销毁,繁忙时创建
- 线程不是越多越好,线程过多也会浪费资源
keepAliveTime非核心线程允许空闲的最大时间TimeUnit unit枚举类型,是keepAliveTime的时间单位BlockingQueue<Runnable> workQueue工作队列- 线程池本质上也是生产者-消费者模型,调用
submit就是在生产任务,线程池里的线程就是在消费任务。 workQueue用来传递数据。可以指定数组或链表,指定capacity,指定是否需要带有优先级或比价规则
- 线程池本质上也是生产者-消费者模型,调用
ThreadFactory threadFactory线程工厂
构造方法的名字只能是类名,要想提供不同的版本就要实现重载,但如果不同版本的参数相同,要想在这种情况下是实现,就要通过工厂模式。
工厂模式用来弥补构造方法的缺陷,核心就是通过静态方法,把new()的过程,各种属性初始化的过程都封装起来。创建对象时调用静态方法。
java
class Point{
double x,y;
public static Point makePointByXY (double x,double y){
Point point = new Point();
//通过x,y给p进行属性设置
point.x = x;
point.y = y;
return point;
}
public static Point makePointByRA(double r,double a){
Point point = new Point();
//通过r,a给p进行属性设置
point.x = Math.cos(a)*r;
point.y = Math.sin(a)*r;
return point;
}
}
public class demo27 {
public static void main(String[] args) {
Point p1 = Point.makePointByXY(1,1);
Point p2 = Point.makePointByRA(1,45);
}
}
通常会把静态方法单独封装一个类,称为工厂类 。工厂类专门负责创建对象,核心职责就是new 实例,把对象的创建逻辑同一收拢。
工厂方法: 写在工厂类里面专门用来创建并返回对象的方法
- 封装成工厂类:
java
class Point{
double x,y;
}
class PointFactory{
public static Point makePointByXY (double x,double y){
Point point = new Point();
//通过x,y给p进行属性设置
point.x = x;
point.y = y;
return point;
}
public static Point makePointByRA(double r,double a){
Point point = new Point();
//通过r,a给p进行属性设置
point.x = Math.cos(a)*r;
point.y = Math.sin(a)*r;
return point;
}
}
public class demo27 {
public static void main(String[] args) {
Point p1 = PointFactory.makePointByXY(1,1);
Point p2 = PointFactory.makePointByRA(1,45);
}
}
threadFactory 就是线程提供的工厂类,可以设置线程的前台/后台,优先级等属性
RejectedExecutionHandler handler 拒绝策略
拒绝策略是线程池参数中最复杂,最重要的参数。
当执行submit时,就会把任务添加到任务队列中,当队列满了再添加就会触发阻塞。
一般我们不希望程序过多的阻塞,对于线程池来说,如果发现入队列操作时,线程满了,不会真的触发"入队列阻塞",而是执行拒绝策略,
拒绝策略:
AbortPolicy:直接抛出异常(默认),这就意味着线程池甚至整个进程都无法工作CallerRunsPolicy:让调用submit的线程自行执行任务DiscardOldestPolicy:丢弃队列最老任务,执行新任务;DiscardPolicy:直接丢弃新任务,不报错。
3. Executors:快速创建线程池
Executors是ThreadPoolExecutor的封装,提供4种常用线程池:
- 固定数量线程池 :
Executors.newFixedThreadPool(5)核心线程数和最大线程数都是5,无临时线程; - 缓存线程池 :
Executors.newCachedThreadPool()没有核心线程,最大线程数是Integer.MAX_VALUE,临时线程空闲60秒销毁; - 单线程池 :
Executors.newSingleThreadExecutor()核心线程和最大线程数都是1,串行执行任务; - 定时线程池 :
Executors.newScheduledThreadPool(3)支持延迟/周期任务。
- 代码示例:固定线程池执行任务
java
public class demo28 {
public static void main(String[] args) {
ExecutorService threadPoll = Executors.newFixedThreadPool(4);
//提交1000个任务
for(int i = 0;i<1000;i++){
int id = i;
threadPoll.submit(()->{
System.out.println("hello "+id+" "+Thread.currentThread().getName());
});
}
}
}
4. 手搓简易线程池
逻辑:1个阻塞队列存任务+N个工作线程循环取任务执行。
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
public class demo29 {
public static void main(String[] args) throws InterruptedException {
// 固定核心线程数为5
MyThreadPoll myThreadPoll = new MyThreadPoll(5);
// 循环提交100个任务到线程池执行
for (int i = 0; i < 100; i++) {
myThreadPoll.submit(() -> {
// 打印当前执行任务的线程名称和任务名称
System.out.println(Thread.currentThread().getName() + " running");
});
}
//此时任务都执行完了,但是整个进程都没有结束,是因为前台线程还没结束,线程池中的线程还在take()等待
//可以使用shutdown,把线程池内的线程全部关闭,但是不能保证线程池内的任务一定都执行完毕
//如果需要等到线程池内任务全部执行完,就要使用awaitTermination
}
}
class MyThreadPoll {
// 阻塞队列:用于存储待执行的Runnable任务
private BlockingQueue<Runnable> queue = null;
public MyThreadPoll(int n) {
// 初始化有界阻塞队列,队列容量1000,用于缓存任务
queue = new ArrayBlockingQueue<Runnable>(1000);
// 循环创建n个工作线程并启动
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
// 死循环:线程持续从队列获取任务,实现线程复用
while (true) {
try {
// take():阻塞获取任务,队列为空时线程阻塞等待
Runnable task = queue.take();
// 执行获取到的任务
task.run();
} catch (InterruptedException e) {
// 线程被中断时抛出运行时异常
throw new RuntimeException(e);
}
}
});
// 启动工作线程
t.start();
}
}
/**
* 提交任务到线程池
* @param task 待执行的任务
* @throws InterruptedException 队列满时put方法会阻塞,阻塞过程被中断抛出异常
*/
public void submit(Runnable task) throws RuntimeException, InterruptedException {
// put():向阻塞队列添加任务,队列满则当前提交线程阻塞
queue.put(task);
}
}
四、定时器
1. 什么是定时器
定时器(Timer)是延迟/周期任务调度工具,类似"闹钟":达到指定时间后,自动执行预设任务。
应用场景:接口超时重试、缓存过期清理、定时数据同步等。
2. Java标准库定时器:Timer类
核心方法
-
schedule(TimerTask task, long delay):延迟delay毫秒后执行1次任务; -
schedule(TimerTask task, long delay, long period):延迟delay毫秒后,每隔period毫秒循环执行。 -
代码示例:延迟3秒执行任务
java
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo {
public static void main(String[] args) {
Timer timer = new Timer();
// 延迟3000毫秒后执行任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时任务执行:" + System.currentTimeMillis());
}
}, 3000);
}
}
注意: Timer中也包含前台线程,不会主动结束进程
3. 手搓简易定时器
- 原理
- 创建一个类来表示任务
- 定时器可以管理多个任务,因此需要优先级队列管理任务,按时间排序
- 实现schedule方法,把任务添加到队列中
- 额外创建一个线程,负责执行队列中的任务。
- 与线程池不同,线程池中的线程只要拿到了任务就可以立即执行,但是此处需要看队首的时间
- 代码实现
java
import java.util.PriorityQueue;
// 定时任务类
class MyTask implements Comparable<MyTask> {
private Runnable runnable;
private long time; // 执行时间戳
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
@Override
public int compareTo(MyTask o) {
return Long.compare(this.time, o.time); // 按执行时间升序
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
}
// 定时器类
public class MyTimer {
private final PriorityQueue<MyTask> queue = new PriorityQueue<>();
private final Object locker = new Object();
public MyTimer() {
// 工作线程:循环扫描并执行任务
new Thread(() -> {
while (true) {
synchronized (locker) {
// 队列为空,等待新任务
while (queue.isEmpty()) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
MyTask task = queue.peek();
long now = System.currentTimeMillis();
// 任务未到执行时间,等待剩余时间
if (now < task.getTime()) {
try {
locker.wait(task.getTime() - now);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
} else {
// 到达执行时间,取出并执行任务
queue.poll();
task.getRunnable().run();
}
}
}
}).start();
}
// 提交延迟任务
public void schedule(Runnable runnable, long delay) {
synchronized (locker) {
queue.offer(new MyTask(runnable, delay));
locker.notify(); // 唤醒工作线程
}
}
// 测试
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(() -> System.out.println("任务1执行:" + System.currentTimeMillis()), 2000);
timer.schedule(() -> System.out.println("任务2执行:" + System.currentTimeMillis()), 1000);
}
}