目录
五. 单例模式
5.1 饿汉模式
5.2 懒汉模式
5.3 饿汉模式和懒汉模式的线程安全问题
六. 阻塞队列
6.1 概念
6.2 生产者消费者模型
6.3 模拟实现一个阻塞队列
七. 定时器
7.1 定时器概念
7.2 模拟实现定时器
八. 线程池
8.1 构造方法参数解析
8.2 模拟实现线程池
理解饿汉,因为太饿了,所以一拿到东西就想吃,这里就是类加载的过程就直接创建唯一实例。
好处:没有线程安全问题
坏处:不像懒汉模式调用才创建实例,这个是类加载就创建实例,所以如果一直没调用,也得创建实例浪费资源。
//饿汉模式
//单例实体
class Singleton {
//唯一实例的本体
private static Singleton instance = new Singleton();
//获取到实例的方法
public static Singleton getInstance() {
return instance;
}
//禁止外部new实例,构造器私有化
private Singleton() {
}
}
public class Demo8 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
}
}
懒汉就是懒加载,在类加载的时候不会去创建实例,在第一次调用的时候才会创建实例。
非必要不创建
//懒汉模式实现单例
class SingletonLazy {
//先置为空
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
//只有调用getInstance方法时,才会去new
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
public class Demo9 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
//s1 和 s2 指向的是同一个实例
System.out.println(s1 == s2);
}
}
显而易见,对于饿汉模式,在多线程中时线程安全的,因为并没有发生修改的操作,而懒汉模式在多线程中就存在线程安全的问题,通过代码可以看见,我们需要判断instance是否为空,然后执行new对象的操作,因为线程的执行是一个抢占式的过程,所以在这里我们需要对懒汉模式进行加锁:
//懒汉模式实现单例 class SingletonLazy { //先置为空 volatile private static SingletonLazy instance = null; public static SingletonLazy getInstance() { //只有调用getInstance方法时,才会去new if (instance == null) { //加锁,保证线程安全问题 synchronized (SingletonLazy.class) { if (instance == null) { instance = new SingletonLazy(); } } } return instance; } private SingletonLazy() { } } public class Demo9 { public static void main(String[] args) { SingletonLazy s1 = SingletonLazy.getInstance(); SingletonLazy s2 = SingletonLazy.getInstance(); //s1 和 s2 指向的是同一个实例 System.out.println(s1 == s2); } }
注:
- 对于 instance 用 volatile 修饰的原因是禁止指令重排序问题;
- 在getInstance方法中,我们为什么不直接对这个方法进行加锁,而是采用一个先判断的形式,再决定要不要对其加锁,这样做的好处是提高程序执行的效率,
阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型.
概念:假设有两个进程(或线程)A、B和一个固定大小的缓冲区,A进程生产数据放入缓冲区,B进程从缓冲区中取出数据进行计算,这就是一个简单的生产者-消费者模型。这里的A进程相当于生产者,B进程相当于消费者。
为什么要用生产者消费者模型?
在多线程开发中,如果生产者生产数据的速度很快,而消费者消费数据的速度很慢,那么生产者就必须等待消费者消费完数据才能够继续生产数据,因为生产过多的数据可能会导致存储不足;同理如果消费者的速度大于生产者那么消费者就会经常处理等待状态,所以为了达到生产者和消费者生产数据和消费数据之间的平衡,那么就需要一个缓冲区用来存储生产者生产的数据,所以就引入了生产者-消费者模式
简单来说,这里缓冲区的作用就是为了平衡生产者和消费者的数据处理能力,一方面起到缓存作用,另一方面达到解耦合作用。
生产者消费者模型代码:
//生产这消费者模型 public class Demo10 { public static void main(String[] args) { //阻塞队列 BlockingQueue
blockingQueue = new LinkedBlockingQueue<>(); //消费者线程 Thread t1 = new Thread(()->{ while (true){ try { int value = blockingQueue.take(); System.out.println("消费元素:"+value); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); //生产者线程 Thread t2 = new Thread(()->{ int value = 0; while (true){ try { System.out.println("生产元素:"+value); blockingQueue.put(value); value++; Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t2.start(); } }
我们可以分为三步来实现:
- 先实现一个普通队列
- 加上线程安全
- 加上阻塞功能
//模拟实现一个阻塞队列 class MyBlockingQueue { private int[] array = new int[10000]; //[head,tail)之间为有效元素 volatile private int head = 0; volatile private int tail = 0; volatile private int size = 0; synchronized public void put(int elem) throws InterruptedException { //判断队列是否满了 if (size == array.length) { this.wait(); } array[tail] = elem; tail++; //判断tail是否达到末尾,如果达到了,把tail置为0,从头开始 if (tail == array.length) { tail = 0; } size++; this.notify(); } synchronized public Integer take() throws InterruptedException { //判断队列是否为空 if (size == 0) { this.wait(); } int value = array[head]; head++; if (head == array.length) { head = 0; } size--; this.notify(); return value; } }
7.1 定时器概念
定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的代码.
- 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
- schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒)
//实现一个定时器,先执行任务2,等待2s后再执行任务1 public class Demo12 { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("任务1"); } },2000);//等待2s后再执行任务1 System.out.println("任务2"); } }
7.2 模拟实现定时器
//模拟实现一个定时器 //表示一个任务 class Mytask implements Comparable
{ public Runnable runnable; public long time; public Mytask(Runnable runnable, long delay) { this.runnable = runnable; //当前时刻的时间戳+定时器参数列表的时间 this.time = System.currentTimeMillis() + delay; } @Override public int compareTo(Mytask o) { //确保每次取出的是时间最小的元素 return (int) (this.time - o.time); } } class MyTimer { private PriorityBlockingQueue queue = new PriorityBlockingQueue<>(); public void schedule(Runnable runnable, long delay) { //根据参数,构造Mytask,插入到队列 Mytask mytask = new Mytask(runnable, delay); queue.put(mytask); synchronized (locker) { locker.notify(); } } //创建锁对象 private Object locker = new Object(); //创建线程,执行任务 public MyTimer() { Thread t = new Thread(() -> { while (true) { try { synchronized (locker) { Mytask mytask = queue.take(); //获取当前时间 long curTime = System.currentTimeMillis(); //判断时间是否到了 if (mytask.time <= curTime) { mytask.runnable.run(); } //时间没到 else { //取出当前执行的任务,重新塞回阻塞队列中 queue.put(mytask); locker.wait(mytask.time - curTime); } } } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); } } public class Demo13 { public static void main(String[] args) { MyTimer myTimer = new MyTimer(); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("任务1"); } }, 1000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("任务2"); } }, 1000); myTimer.schedule(new Runnable() { @Override public void run() { System.out.println("任务3"); } }, 1000); System.out.println("最开始的任务"); } }
线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。我们可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。
jdk1.8官方文档关于ThreadPoolExecutor的构造方法的参数解析:
corePoolSize:核心线程数(相当于一个公司的正式员工);
maximumPoolSize:最大线程数(相当于一个公司的正式员工+实习生);
如果当前任务比较多的时候,线程池会多创建一些 “临时线程” 去帮助解决任务;
如果任务比较少,线程池会多出来的 “临时线程” 给销毁掉,但是核心线程数不会销毁;
long keepAliveTime:描述了 “临时线程” 最大存活时间,超出这个时间,就会被销毁;
TimeUnit unit:是数值的单位;
BlockingQueue
ThreadFactroy threadFactory:线程工厂,用来创建线程;
RejectedExecutionHandler handler:线程池的拒绝策略;如果线程池满了,继续往里面添加任务,就是触发拒绝策略
class MyThreadPool {
private BlockingQueue queue = new LinkedBlockingQueue();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
try {
while (true) {
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
}
public class Demo15 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int ret = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("任务 " + ret);
}
});
}
}
}