【多线程经典案例】- 单例/阻塞队列/定时器/线程池

经典案例

  • 一、单例模式
  • 二、阻塞队列
  • 三、定时器
  • 四、线程池

一、单例模式

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

  • 饿汉式 : 直接就将对象创建出来,不管后面会不会用到,需要时就直接返回。因为在类加载时就创建好对象了,后续不需要再创建,相当于只有读操作,所以是 线程安全

    // 通过 Singleton 这个类来实现
    class Singleton{
        // 这个 instance 就是该类的唯一实例
        private static Singleton instance = new Singleton();// 类成员
        // 为了防止再获得他的对象 (new),将构造方法私有化
        private Singleton(){}
    
        // 提供方法让外界拿到 instance 对象
        public static Singleton getInstance(){
            return instance;
        }
    }
    
  • 懒汉式: 当需要用到时,再创建实例,后续不再创建

    // 通过 Singleton 这个类来实现
    class Singleton{
        // 这个 instance 就是该类的唯一实例
        private static volatile Singleton2 instance = null;// 类成员
        // 为了防止再获得他的对象,将构造方法私有化
        private Singleton(){}
    
        // 提供方法让外界拿到 instance 对象
        // 只有当真正要用到实例时,才会创建实例
        public static Singleton getInstance(){
        	// 判断是否已经创建过对象了
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    但是我们可以发现在懒汉式中, 该方法有读,还有修改对象,而且不是原子的,线程不安全。

如何实现一个线程安全的单例模式?

  • 加锁(synchronized),将读写操作封装成一个原子操作

    synchronized (Singleton.class){
        // 看是否要创建实例
        if (instance == null) {
            // 初始化之后,就不会再进行修改,线程安全
            instance = new Singleton();
        }
    }
    
  • 这样虽然没问题,但是频繁的获取和释放锁对象,会造成很大的开销,所以在外层加上一层判定,如果已经创建好了实例,就不去竞争锁,直接返回。

    // 看要不要竞争锁
    if (instance == null) {
        synchronized (Singleton.class){
            // 看是否要创建实例
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    
  • 当多个线程同时读 instance ,会有内存可见性问题,当还没有创建好对象时,多个读到的都是 null ,即使多个线程中有一个线程创建了instance 实例,其他线程还是会去竞争锁对象。所以加上 volatile 关键字,避免无用的锁竞争。

    private static volatile Singleton instance = null;
    

完整代码:

// 通过 Singleton 这个类来实现
class Singleton{
    private static volatile Singleton instance = null;
    private Singleton() {}

    public static Singleton getInstance(){
        // 看要不要加锁
        if (instance == null) {
            synchronized (Singleton.class){
                // 看是否要创建实例
                if (instance == null) {
                    // 初始化之后,就不会再进行修改,线程安全
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

二、阻塞队列

阻塞队列是一种特殊的队列,除了具有队列的性质外,还具有阻塞的功能。例如下面的典型案例 – 生产者消费者模型

  • 当队列满时,继续入队列就会阻塞,直到有其他线程从队列中取走元素
  • 当队列空时,继续出队列也会阻塞,直到有其他线程往队列中插入元素
    【多线程经典案例】- 单例/阻塞队列/定时器/线程池_第1张图片
  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
    比如 “双十一” 当天,淘宝的服务器同一时刻可能会收到大量的支付请求。如果直接处理这些支付请求,可能扛不住。就将这些请求放到一个阻塞队列中,然后再由消费者线程慢慢来处理每个请求。这样做可以有效进行 “削峰”,防止服务器被突然到来的一波请求直接冲垮。
  2. 阻塞队列也能使生产者和消费者之间 解耦 (因为有一个缓冲区,生产者和消费者不需要有直接联系,通过缓冲区即可实现)
    例如买手机,消费者不需要知道是谁生产的手机,生产者也不需要知道是谁买手机。两者没有直接的联系,而是通过手机店这样的类似与中介的地方进行交易,一定程度上解除了耦合。

java标准库中的阻塞队列(BlockingDeque)

  • BlockingQueue 是一个接口,真正实现的类是 LinkedBlockingQueue
  • put 方法用于阻塞式的入队列,take 用于阻塞式的出队列
  • BlockingQueue 也有 offer,poll,peek 等方法,但是这些方法不带有阻塞特性
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
queue.put("hello");// 入队列
String s = queue.take();// 出队列

实现一个阻塞队列: 普通循环队列 + 线程安全( synchronized) + 阻塞功能( wait、notify)

  • 当队列满时,put 操作阻塞等待(wait),直到被 take 的操作(notify)唤醒
  • 当队列空时,take 操作阻塞等待(wait),直到被 put 的操作(notify)唤醒
    【多线程经典案例】- 单例/阻塞队列/定时器/线程池_第2张图片
// 实现一个阻塞队列
class MyBlockingQueue{
    // 数组 -- 循环队列
    private int[] data = new int[1000];
    private int size = 0;// 元素个数
    private int head;// 队首下标
    private int tail;// 队尾下标

    // 入队列
    // 每个都在操作公共变量,就给整个方法加锁
    public synchronized void put(int val) throws InterruptedException {
        if (size == data.length) {
            // 队满,阻塞
            wait();
        }
        data[tail++] = val;
        // tail 达到末尾
        if (tail >= data.length) {
            tail = 0;
        }
        size++;
        // 入队列成功,队列非空,唤醒 take()
        // 如果take()处于阻塞态,就能唤醒,不处于阻塞态,也没有副作用
        notify();
    }
    // 出队列
    public synchronized Integer take() throws InterruptedException {
        if (size == 0) {
            wait();
        }
        int val = data[head];
        head++;
        if (head == data.length) {
            head = 0;
        }
        size--;
        // take成功之后,队列非满,唤醒 put的等待
        notify();
        return val;
    }
}

基于上述阻塞队列,实现一个简单的生产者消费者模型

private static MyBlockingQueue queue = new MyBlockingQueue();
    public static void main(String[] args) {

        // 生产者线程
        Thread producer = new Thread(() -> {
            int num = 0;
            while (true) {
                try {
                    System.out.println("生产了 " + num);
                    queue.put(num);
                    num++;
                    // 生产慢
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
        
        // 消费者线程
        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    // 消费慢
                    //Thread.sleep(500);
                    int num = queue.take();
                    System.out.println("消费了 " + num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
    }

三、定时器

什么是定时器

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

  • 标准库中提供了一个 Timer 类,Timer 类的核心方法为 schedule

  • schedule 包含两个参数,第一个参数指定要执行的任务,第二个参数指定多长时间之后执行 (毫秒)

    Timer timer = new Timer();
    //			new TimerTask()表示一个任务
    timer.schedule(new TimerTask(){
         @Override
         public void run() {
             System.out.println("我是要执行的任务");
         }
     },3000);// 3000ms后执行该任务
    

Timer 类的实现:

  • 管理很多任务
    1.描述一个任务(创建一个类 MyTask)

    // 表示一个任务
    class MyTask{
        // 任务具体要干啥
        private Runnable runnable;
        // 任务什么时候干
        private long time;
    
        // delay 是一个时间间隔,不是绝对的时间
        public MyTask(Runnable runnable,long delay) {
            this.runnable = runnable;
            this.time = System.currentTimeMillis() + delay;
        }
    
        public void run(){
        //	通过这个方法,执行任务
            runnable.run();
        }
        public long getTime(){
            return this.time;
        }
    }
    

    2.组织一个任务 (数据结构)

    1. 假设有很多任务,10min后写作业,20min后打游戏,200min后出去玩。我们应该用什么数据结构组织能按时间顺序拿到任务呢?
    2. 按照时间先后执行 – 优先级队列(堆)

    因为堆要进行比较,所以放入的元素如果是自定义类型,要指定比较方式,所以让MyTask类实现Comparable接口,并重写compareTo()方法指定比较方式(按时间顺序排序)

    // 表示一个任务
    class MyTask implements Comparable<MyTask>{
        // ...... 前面一样
        // 重写compareTo方法,指定比较方式
        @Override
        public int compareTo(MyTask o) {
            return (int)(this.time - o.time);
        }
    }
    
  • 在MyTimer类中,使用一个 带有阻塞功能的优先级队列 来放入任务

    class MyTimer{
        // 带有阻塞功能的优先级队列 -- 要考虑线程安全问题,可能在多个线程进行注册任务,同时还有线程来执行
        private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    }
    
  • schedule方法 – 按照传入的参数,封装一个MyTask 对象,放入优先级队列中

    // 把任务放进队列
    public void schedule(Runnable runnable,long delay){
            MyTask task = new MyTask(runnable, delay);
            queue.put(task);
        }
    
  • 执行时间到了的任务 – 需要有一个线程去扫描,看是否有任务到了执行时间
    但是这里不能让该线程一直去扫描,例如“3:50”该做作业,从“3:30”开始,就一直拿手机看时间是否到了,这显然是不科学的,属于“忙等”。
    正确做法应该是:看排在最前面的任务,执行时到到没,没到则等待该任务的对应时间wait(time),然后再看是否到了,相当于“定闹钟”操作

    private Object locker = new Object();// 一个锁对象
    // 需要执行最靠前的任务
    // 需要有一个线程来检查 小根堆的顶元素(任务),看是否需要执行了
    public MyTimer(){
        Thread t = new Thread(() -> {
            while (true){
                try {
                    // 取出堆顶任务
                    MyTask task = queue.take();
                    // 看是否到执行时间了
                    long curTime = System.currentTimeMillis();
                    // 如果时间还没到
                    if (curTime < task.getTime()){
                        // 将任务塞回优先级队列
                        queue.put(task);
                        // 等待相应时间 wait()
                        synchronized (locker){
                            locker.wait(task.getTime() - System.currentTimeMillis());
                            }
                    }else {
                        // 时间到了 --- 执行该任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
    
  • 这里又出现了一个问题:假设本来有一个阻塞队列,现在插入一个新的任务task4(5min)
    【多线程经典案例】- 单例/阻塞队列/定时器/线程池_第3张图片

可以看到,新任务的执行时间更靠前,所以需要唤醒一下线程,让他再扫描一下堆顶元素,看是否到执行时间了,所以在schedule方法中,需要执行唤醒操作

public void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable, delay);
        queue.put(task);
        // 每次任务插入成功后,唤醒一下线程,让他检查一下堆顶元素是否到执行时间了
        synchronized (locker){
            locker.notify();
        }
    }

整体代码

// 描述一个任务
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;
    }

    public void run(){
        runnable.run();
    }
    public long getTime(){
        return this.time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}
// 定时器类
class MyTimer{
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable, delay);
        queue.put(task);
        synchronized (locker){
            locker.notify();
        }
    }
    private Object locker = new Object();
    public MyTimer(){
        Thread t = new Thread(() -> {
            while (true){
                try {
                    MyTask task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (curTime < task.getTime()){
                        queue.put(task);
                        synchronized (locker){
                            locker.wait(task.getTime() - System.currentTimeMillis());
                        }
                    }else {
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

四、线程池

简单来说线程池就是,把线程创建好,放在池子里。线程用完了,不是还给系统,而是放回池子里,以备下一次用。后面需要用线程时,不必从系统这边申请,而是直接从池子里拿,一定程度上减少了开销。

标准库中线程池
【多线程经典案例】- 单例/阻塞队列/定时器/线程池_第4张图片

public static void main(String[] args) throws InterruptedException {
    // 创建一个固定线程的线程池
    ExecutorService pool = Executors.newFixedThreadPool(10);
    // 创建一个自动扩容的线程池
    // Executors.newCachedThreadPool();
    // 创建一个只有一个线程的线程池
    // Executors.newSingleThreadExecutor();
    // 创建一个带有定时器功能的线程池
    // Executors.newScheduledThreadPool();
    for (int i = 0; i < 100; i++) {
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello threadPoll");
            }
        });
    }
}

实现一个线程池

  • 描述一个任务(Runnable)

  • 组织任务(BlockingQueue)

    private static BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
    
  • 描述工作线程

    // 描述一个工作线程,工作线程的功能就是从队列中取任务来执行
    static class Worker extends Thread{
          @Override
          public void run() {
              // 拿到上面的队列,取出任务执行
              while (true) {
                  try {
                      // 循环获取任务
                      // 如果队列空,则会阻塞
                      Runnable runnable = MyThreadPool.queue.take();
                      runnable.run();// 执行该任务
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
    
  • 组织工作线程

    // 创建一个数据结构来组织线程
    private List<Thread> workers = new ArrayList<>();
     public MyThreadPool(int n) {
         // 创建若干个线程,放到上述数组
         for (int i = 0; i < n; i++) {
             Worker worker = new Worker();
             worker.start();
             workers.add(worker);
         }
     }
    
  • 需要实现往线程池添加任务

    // 创建一个方法,允许程序员放任务到线程池
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    

整体代码

class MyThreadPool {
    private static BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
    
    static class Worker extends Thread{
        @Override
        public void run() {
            while (true){
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    private List<Thread> workers = new ArrayList<>();
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker();
            worker.start();
            workers.add(worker);
        }
    }

    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

你可能感兴趣的:(单例模式,java,阻塞队列,定时器,线程池)