【Java SE】多线程(三):单例模式,阻塞队列,线程池与定时器

文章目录

  • 一、单例模式
    • [1. 什么是单例模式](#1. 什么是单例模式)
    • [2. 饿汉模式](#2. 饿汉模式)
    • [3. 懒汉模式](#3. 懒汉模式)
    • [4. 懒汉模式的线程安全问题](#4. 懒汉模式的线程安全问题)
  • 二、阻塞队列
    • [1. 什么是阻塞队列](#1. 什么是阻塞队列)
    • [2. Java标准库阻塞队列:BlockingQueue接口](#2. Java标准库阻塞队列:BlockingQueue接口)
    • [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()分为三步:

  1. 分配内存;
  2. 初始化实例;
  3. 赋值给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. 生产者-消费者模型

优势

  1. 解耦合 (两个线程或两个服务器之间)

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

如果在A和B之间加入阻塞队列,A和队列交互,B也和队列交互,A和B不再直接交互,A和B就实现了解耦合。

消息队列的功能非常重要,多数情况会把队列单独部署成一个服务,独立的服务的阻塞队列,称为消息队列

队列的功能比较固定,不涉及大规模改动,所有不需要担心A与队列以及B与队列增加耦合的情况

  1. 削峰填谷
  • A这种上游的服务器,尤其入口服务器,单个请求消耗的资源少;
  • B这种下游服务器,承担更重的任务量,包括复杂的计算和存储工作,单个请求消耗的资源更多。
  • 对于队列服务器,针对单个请求,只需要做存储和转发工作,可以承受很高的请求量。

这样利用生产者-消费者模型,B不需要关心队列中的数据量有多少,只需要按照自己的节奏依次处理请求即可。

劣势

  1. 引入队列后,需要更多的机器进行部署,生产环境的结构更复杂,管理起来更麻烦
  2. 对比服务器直接通信,引入队列效率有一定影响

代码实现

  • 需求:生产者线程生产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 // 拒绝策略:任务超负载时的处理方式
)
  1. corePoolSize:核心线程数
    • Java的线程池里面包含多个线程,可以动态调整,任务多的时候,自动扩容线程池;任务少的时候,把额外的线程干掉节省资源。
    • 核心线程数可以理解为线程池中最少的线程数,线程池一创建,这些线程就会随之创建。直到线程池销毁,这些线程才会销毁
  2. maximumPoolSize 最大线程数,核心线程数+非核心线程数
    • 非核心线程在不繁忙时销毁,繁忙时创建
    • 线程不是越多越好,线程过多也会浪费资源
  3. keepAliveTime 非核心线程允许空闲的最大时间
  4. TimeUnit unit枚举类型,是keepAliveTime的时间单位
  5. 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种常用线程池:

  1. 固定数量线程池Executors.newFixedThreadPool(5) 核心线程数和最大线程数都是5,无临时线程;
  2. 缓存线程池Executors.newCachedThreadPool() 没有核心线程,最大线程数是Integer.MAX_VALUE,临时线程空闲60秒销毁;
  3. 单线程池Executors.newSingleThreadExecutor() 核心线程和最大线程数都是1,串行执行任务;
  4. 定时线程池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. 手搓简易定时器

  • 原理
  1. 创建一个类来表示任务
  2. 定时器可以管理多个任务,因此需要优先级队列管理任务,按时间排序
  3. 实现schedule方法,把任务添加到队列中
  4. 额外创建一个线程,负责执行队列中的任务。
    • 与线程池不同,线程池中的线程只要拿到了任务就可以立即执行,但是此处需要看队首的时间
  • 代码实现
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);
    }
}
相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第115题】【并发篇】第15题:说一下悲观锁和乐观锁的区别?
java·开发语言·面试
lijgvnns1 小时前
个人AI编程工具的vibe coding实践:从爬虫到导出Excel的全流程
开发语言·javascript·ecmascript
心之伊始1 小时前
Spring Boot Actuator + Micrometer 实战:自定义业务指标并接入 Prometheus 观测接口耗时
java·spring boot·prometheus·actuator·micrometer
এ慕ོ冬℘゜1 小时前
jQuery 高可用多图上传组件(企业级封装 + 踩坑全解 + 可直接上线)
前端·javascript·jquery
Full Stack Developme2 小时前
Spring Integration 教程
java·后端·spring
kymjs张涛2 小时前
一个月,纯VibeCoding,全平台云笔记APP
前端·javascript·后端
摇滚侠2 小时前
MyBatis 入门到项目实战 MyBatis 分页插件 65-66
java·开发语言·sql·mybatis
星辰_mya2 小时前
autowired和resource区别
java·后端·spring·架构·原理