Java多线程常见案例分析线程池与单例模式及阻塞队列

 

一、单例模式

设计模式:软件设计模式

是一套被反复使用、多数人知晓、经过分类编目、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

单例模式:是设计模式的一种。保证某个类在程序中只存在唯一一份实例,不会创建出多个实例。单例模式的具体实现分为“懒汉”和“饿汉”两种。

构造方法必须是私有的,保证该类不能在类外被随便创建。

1、饿汉模式

类加载的同时,创建实例。(缺点是无论是否使用都会创建对象,比较占空间)

//类加载的时候创建对象,确保只能有一个实例对象
class Singleton {
  private static Singleton instance = new Singleton();
  //私有的构造方法
  private Singleton() {}
  //只能通过getInstance()方法获取到同一个实例对象
  public static Singleton getInstance() {
      return instance;
  }
}

2、懒汉模式(单线程)

类加载的时候不创建实例,第一次使用的时候才创建实例。

(缺点:线程不安全,如果存在多个线程并发并行执行,可能创建多个实例,所以只适用于单线程)

class Singleton {
  private static Singleton instance = null;
  private Singleton() {}
  public static Singleton getInstance() {
      //第一次使用时,创建实例
      if (instance == null) {
          instance = new Singleton();
      }
      //后面使用时,直接返回第一次创建的实例
      return instance;
  }
}

3、懒汉模式(多线程)

上面的懒汉模式存在线程安全问题,如果多个线程同时调用getInstance()方法,可能创建多个实例。所以在多线程时,我们需要使用synchronized改善线程安全问题。

class Singleton {
  private static Singleton instance = null;
  private Singleton() {}
  //加锁保证不会有多个线程同时访问改代码块
  public synchronized static Singleton getInstance() {
      if (instance == null) {
          instance = new Singleton();
      }
      return instance;
  }
}

对于以上代码,虽然保证了线程安全,但是对于懒汉模式,只有在第一次调用时才会创建实例,大多数境况下只进行读操作,如果对代码块整体加锁,程序执行的效率会大大降低。我们可以对上面的程序进一步优化,对于读操作,我们使用volatile修饰变量;只给写操作的代码块加上锁即可。

【单例模式懒汉模式多线程的进一步优化】双重if判定

class Singleton {
  //使用volatile修饰变量
  private static volatile Singleton instance = null;
  private Singleton() {};
  public static Singleton getInstance() {
      if (instance == null) {
          //只给写操作的相关代码加锁
          synchronized (Singleton.class) {
              //需要双重if判断,防止在多线程中加锁前instance发生变化
              if (instance == null) {
                  instance = new Singleton();
              }
          }
      }
      return instance;
  }
}

写操作加锁,保证线程安全;

如果已经实例化,进行读操作,保证多个线程并发并行执行,保证效率。

 

二、阻塞队列

阻塞队列是什么?

阻塞队列是一种特殊的队列。也遵守“先进先出”的原则。

阻塞队列是一种线程安全的数据结构:

  • 当队列满的时候,继续入队队列就会阻塞,知道有其他线程从队列中取走元素;
  • 当队列空的时候,继续出队也会阻塞,直到其他线程往队列中插入元素。

阻塞队列的一个经典应用场景就是“生产者消费者模型”。

标准库中的阻塞队列:

  • BlockingQueue是一个接口,真是实现的是类是:LinkedBlockingQueue。
  • put方法用于阻塞式的入队列,take用于阻塞式的出队列。
  • BlockingQueue也有offer、poll、peek方法,但是不具有阻塞特性。

阻塞队列的实现

  • 通过循环队列实现;
  • 使用synchronized进行加锁控制
  • put插入元素,如果队列满了,就进行wait(要在循环中进行wait,多线程情况下可能唤醒多个线程,所以唤醒后队列可能还是满的)
  • take取出元素,如果队列为空,就wait(循环中wait)
public class BlockingQueue{
  //使用循环数组来实现阻塞队列
  private int[] array;
  //队列中已经存放元素的个数
  private int size;
  //放入元素的下标
  private int putIndex;
  //取元素的下标
  private int takeIndex;
  //在构造方法中指定队列的大小
  public BlockingQueue(int capacity){
      array=new int[capacity];
  }
  /*放元素:需要保证线程安全,如果队列满了,线程进入等待*/
  public synchronized void put(int m) throws InterruptedException {
      //队列满,线程等待
      if(size==array.length){
          //需要注意的是,进行等待的是当显得实例对象,不是类对象
          this.wait();
      }
      //放元素,同时更新下标
      array[putIndex]=m;
      putIndex=(putIndex+1)%array.length;
      size++;
      //通知等待的线程
      notifyAll();
  }
  /*取元素:保证线程安全。如果队列为空,线程等待*/
  public synchronized int take() throws InterruptedException {
      //队列为空,线程等待
      if(size==0){
          this.wait();
      }
      //取元素,同时更新下标
      int ret=array[takeIndex];
      takeIndex=(takeIndex+1)%array.length;
      size--;
      //通知等待的线程
      notifyAll();
      return ret;
  }
}

生产者消费者模型

生产者消费者模型就是通过一个容器来解决生产者和消费者之间的强耦合问题。

生产者和消费者之间不直接通信,而通过阻塞队列来实现通讯,所以生产者生产完数据不需要等待消费者来处理,直接扔给阻塞队列。消费者也不需要去找生产者,而是直接从阻塞队列中取。

  • 阻塞队列相当于一个缓冲区,平衡了消费者和生产者的处理能力;
  • 阻塞队列也能使生产者和消费者之间“解耦”。

耦合和解耦:

  • 耦合指的是两个类之间联系的紧密程度。强耦合(表示类之间存在着直接的关系)。弱耦合(在两个类的中间加入一层,将原来的之间关系变成间接关系,使得两个类对中间层是强耦合,两个类之间变成了弱耦合。
  • 解耦:降低耦合度,也就是将强耦合变成弱耦合的过程。

 

三、线程池

池:字符串常量池(类似缓存)、数据库连接池等

线程池:初始化的时候就创建一定数量的线程【不同的从线程池的阻塞队列中取任务(消费者)】【在其他线程中提交任务到线程池(生产者)】

优点:

线程的创建和销毁都有一定的代价,使用线程池就可以重复使用线程来执行多组任务。(如果线程不再使用,并不是真正的将线程释放,而是放到一个“池子”中,下次如果需要用到线程直接从池子中取,不必通过系统来创建)

1、创建线程池的的方法

(1)ThreadPoolExecutor

提供了更多的可选参数,可以进一步细化线程池行为的设定。

以第三个构造方法为例:

  1. corePoolSize:表示核心线程的数量
  2. maximumPoolSize:最大线程数(核心线程+临时线程)
  3. keepAliveTime:允许临时线程空闲的时间(如果超过该时间临时线程还是没有任务执行,就被销毁)
  4. unit: keepaliveTime的时间单位
  5. workQueue:传递任务的阻塞队列
  6. threadFactory:规定创建线程的标准
  7. RejectedExecutionHandler:拒绝策略,如果阻塞队列已满,再传进来任务该怎么办

【1】AbortPolicy():超过负荷,直接抛出异常(默认的拒绝策略,使用其他不带拒绝策略的构造方法时的默认参数)

【2】CallerRunsPolicy():调用者负责处理

【3】DiscardOldestPolicy():丢弃队列中最老的任务

【4】DiscardPolicy():丢弃新来的任务

创建线程池如下:

        //使用ThreadPoolExecutor创建线程池
      ThreadPoolExecutor threadPool1=new ThreadPoolExecutor(
              5,
              10,
              3,
              //自由线程无任务时最大存活时间单位:分
              TimeUnit.MINUTES,
              //一般不使用无边界的阻塞队列,内存有限
              new ArrayBlockingQueue<>(100),
              //规定创建线程的标准
              Executors.defaultThreadFactory(),
              //拒绝策略:一般最多使用CallerRunsPolicy(),或自己实现
              new ThreadPoolExecutor.CallerRunsPolicy()
      );

(2)Executors(快捷创建线程池的API)

Executors创建线程的几种方式:

  • newFixedThreadPool:创建固定线程数的线程池(没有临时线程)
  • newCachedThreadPool:创建线程数目动态增长的线程池(缓存的线程池,没有核心线程,全是临时线程)
  • newSingleThreadExecutor:创建只包含单个线程的线程池
  • newScheduledThreadPool:设定延迟时间后执行任务,或者定期执行命令(计划线程池)

创建线程池如下:

        //Executors的四种创建线程的方法
      //没有临时线程的线程池
      ExecutorService threadPool2= Executors.newFixedThreadPool(10);
      //线程数目动态增长的线程池
      ExecutorService threadPool3=Executors.newCachedThreadPool();
      //创建单个线程的线程池
      ExecutorService threadPool4=Executors.newSingleThreadExecutor();
      //计划线程池
      ExecutorService threadPool5=Executors.newScheduledThreadPool(7);

2、线程池的工作流程

线程池工作流程

使用线程池:

创建线程池

提交任务:

【1】submit(Runnable task)

【2】execute(Runnable task)

关于Java多线程常见案例分析线程池与单例模式及阻塞队列的文章就介绍至此,更多相关Java线程池内容请搜索编程宝库以前的文章,希望以后支持编程宝库

 一、线程安全(重点)1、线程安全概念在多线程的情况下,需要考虑多个线程并行并发执行:此时多个线程之间的代码是随机执行的。如果多线程环境下代码的运行结果是符合我们的预期的,即在单线程情况下 ...