目录
1.单例模式
(1)饿汉模式
(2)懒汉模式
前言
懒汉式1-synchronized加锁
懒汉式2-双重if保障
懒汉式3-volatile防止误判
2.生产者消费者模型
(1)阻塞队列
(2)优点
解耦合
削峰填谷
(3)代价
(4)代码案例
模拟实现阻塞队列
3.线程池
(1)操作系统的两种状态
内核态(Kernel Mode)
用户态(User Mode)
(2)实现线程池
(3)Java标准库提供的线程池
构造方法
注意事项
(4)线程池优点
4.定时器
(1)标准库中的定时器
(2)实现定时器
1.单例模式
设计模式:即针对某些特定的场景,大佬们设计出来的一些固定套路来供我们使用
单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例.这一点在很多场景上都需要.比如JDBC中的DataSource实例就只需要一个.
单例模式具体的实现方式有很多.最常见的是"饿汉"和"懒汉"两种.
单例模式前提是"一个进程中",如果是有多个Java进程,自然每个进程中都可以有一个实例了
(1)饿汉模式
“饿汉”二字我们可以理解为迫切的意思,在类加载的时候就会创建这个单例的实例 (单个对象)
class Singleton{private static final Singleton instance = new Singleton();private Singleton() {};public static Singleton getInstance() {return instance;} }public class Demo1 {public static void main(String[] args) {Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1 == s2);} }
- static修饰instance成员变量就是“类成员”,类成员的初始化就是在Singleton这个类被加载的时候(程序启动的时候);它属于类本身,而不是类的某个特定对象。因此,这个实例变量在内存中只有一份拷贝,被类的所有实例共享
当Singleton类被JVM加载到内存中时,就会创建一个Singleton的实例,并将其赋值给instance变量。由于instance是static的,所以无论创建多少个Singleton的实例,instance都指向同一个对象。- 我们这里创建的getInstance方法就是为了保证每次访问Singleton类时要通过该方法来获取实例,避免被人new对象,这也是单例模式解决的主要问题
- private Singleton() {};这个构造方法意味着在类的外面,就无法调用构造方法,无法初始化也就无法创建实例了
(2)懒汉模式
前言
首次调用getInstance为null时就会进入if语句来实例化对象,之后再调用getInstance时就不再重新创建直接返回
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy() {};public static SingletonLazy getInstance() {if (instance == null) {instance = new SingletonLazy();}return instance;} }
这样看似乎没有什么问题,当我们将它与多线程结合起来就会存在线程安全问题
这里的if语句和下面的实例化赋值修改的语句就会存在线程安全问题,这两个语句之间可能会存在线程切换的问题
无论new操作是在本线程,还是其他线程进行的此处的 return 的值都是Instance内存中的最新值
懒汉式1-synchronized加锁
如何解决呢?进行加锁操作,安全性问题时发生在if判断语句和内部的赋值语句的,在这之间可能会发生线程的切换,所以我们需要将这两步具有原子性,对其进行加锁
class SingletonLazy{private static SingletonLazy instance = null;private static Object locker = new Object();private SingletonLazy() {};public static SingletonLazy getInstance() {synchronized (locker) {if (instance == null) {instance = new SingletonLazy();}}return instance;} }
加锁之后便解决了线程安全问题,但与此同时也可能带来阻塞的问题,在我们new操作执行过一次后,if语句就不再发挥作用,之后都是直接返回该对象,后续的代码就只是单纯的读操作,此时之后的每次加锁就是无用功,每次都会触发加锁的操作,虽然之后不会发生线程安全问题,但却因为加锁这一步骤产生了阻塞进而影响到了性能
懒汉式2-双重if保障
因此我们需要再次进行判断,当第一次调用getInstance方法时,instance为null,此时就需要加锁,当之后再访问时如果instance为空就在进入判断,不为空就不再进入
synchronized会使代码出现阻塞,这一旦阻塞之后,啥时候恢复执行,中间可能是"沧海桑田"在这个过程中,很可能其他线程就把这个值给修改了
因为是多线程,两行代码之间可能会穿插其他代码class SingletonLazy{private static SingletonLazy instance = null;private static Object locker = new Object();private SingletonLazy() {};public static SingletonLazy getInstance() {if (instance == null) {synchronized (locker) {if (instance == null) {instance = new SingletonLazy();}}}return instance;} }
- 外面的if语句是判断是否需要加锁
- 里面的if语句是判断是否需要创建对象
懒汉式3-volatile防止误判
此时依然存在问题,我们上篇文章说过开发编译器的大佬们为了提高大家的效率会进行优化,这里就进行了指令重排序,从而引起了线程安全问题,此时为了防止误判,我们依旧加volatile关键字进行标记即可
代码中的 instance = new SingletonLazy();可分为三步
- 分配内存空间
- 执行构造方法
- 内存空间的地址赋值给引用变量
编译器会先执行第一步,2和3谁先执行不确定
对于单线程来说,先执行2还是先执行3,本质上是一样的
这里我们可以这样理解,这三步我们可以看作
- 买房
- 装修
- 收房拿钥匙
对应顺序123步骤则拿到的就是精装房(已经装修好了),而132则是毛坯房(之后自己装修)
class SingletonLazy{private static volatile SingletonLazy instance = null;private static Object locker = new Object();private SingletonLazy() {};public static SingletonLazy getInstance() {if (instance == null) {synchronized (locker) {if (instance == null) {instance = new SingletonLazy();}}}return instance;} }
2.生产者消费者模型
(1)阻塞队列
阻塞队列是一种特殊的队列,也遵循“先进先出”的原则
阻塞队列能是一种线程安全的数据结构,(标准库中原有的队列Queue和其子类默认都是线程不安全的)并且具有以下特性:
- 当队列满的时候,继续入队列就会阻塞,直到有其它线程从队列中取走元素
- 当队列空的时候,继续出队列也会阻塞,直到有其它线程往队列中插入元素
阻塞队列的一个典型的应用场景就是“生产者消费者模型”,这是一种典型的开发模型
生活场景举例:
服务器之间的通信举例:
我们当下的分布式系统也是如此,并不是一个系统解决所有问题,而是多个系统之间相互调用
(2)优点
解耦合
- 服务器之间的“解耦合”(理想状态下希望服模块之间的关联程度/影响程度较低,也就是低耦合)
此时服务器之间的耦合性就较高,若一个服务器挂了,与其相连的服务器也会是受到较大的影响,所以此时我们就需要引入阻塞队列使用生产者消费者模型来降低耦合
此时AB之间通过阻塞队列,达到了解耦合的效果,但是A和队列,B和队列之间是否又引入了新的耦合呢?答案是没有,我们通常所谈到的阻塞队列是代码中的一个数据结构,由于这个东西太好用,以至于会把这样的数据结构单独封装成一个服务器程序并且在单独的服务器机器上进行部署,此时这样的阻塞队列就叫做“消息队列”(Message Queue,MQ),消息队列是成熟的产品,代码不会频繁修改,代码是稳定的,所以相互之间的逻辑基本一次就顶下来了
削峰填谷
通过中间的阻塞队列,可以起到“削峰填谷”的效果,在遇到请求量激增的情况下可以保护下游服务器不会被请求冲垮
消息队列(阻塞队列)服务器通信过程中,也是能起到的削峰填谷
为什么一个服务器收到的请求越多,就可能会挂?
一台服务器就类似于是一台电脑,上面就提供了一些硬件资源(CPU,内存,硬盘,网络带宽等),尽管配置再好,硬件资源也是有限,当服务器每次收到一个请求,处理这个请求的过程中就都需要执行一系列的代码,在执行这些代码的过程中就会需要消耗一定的硬件资源,当这些请求消耗的总的硬件资源超过了机器能提供的上限,那么此时机器就会出现问题(卡死,程序崩溃等)
- 资源耗尽:内存耗尽,CPU过载,磁盘IO瓶颈(在处理请求时,服务器可能需要频繁地进行磁盘读写操作。如果请求量过大,磁盘IO会成为瓶颈,导致请求处理速度下降,甚至导致服务器崩溃)
- 并发连接数有限(每个服务器的并发连接数都是有限的)
- 资源争夺(请求量过大,资源争夺会变得更加激烈,导致处理请求的时间变长,进而影响服务器的稳定性和性能)
- 设计局限(有的服务器不具备高并发能力)
- 外部因素(网络不稳定,温度过高等)
在请求激增的时候,A为啥不会挂?队列为啥不会挂?反而是B更容易挂呢??
A的角色是一个“网关服务器”,收到客户端的请求再把请求转发给其他服务器,这样服务器里面的代码做的工作比较简单(单纯的数据转发)消耗的硬件资源通常更少,因此在处理同一个请求时,消耗的资源更少,同样的配置下就能支持更多的请求处理,
同理,队列其实也是比较简单的程序,单位请求消耗的硬件资源也是比较少的
B这个服务器是真正工作的服务器,要真正完成一系列的业务逻辑,这一系列的工作代码量非常大,消耗的时间很多,消耗的硬件资源也是更多的
(3)代价
- 硬件成本增加
- 通信时间变长
(4)代码案例
public class Demo3 {public static void main(String[] args) throws InterruptedException {//阻塞队列的容积为3BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);queue.put(111);System.out.println("put成功");queue.put(222);System.out.println("put成功");queue.put(333);System.out.println("put成功");//阻塞队列的容量为3,当添加第4个数据时,就会发生阻塞等待queue.put(333);System.out.println("put成功");} }
模拟实现阻塞队列
class MyBlockingQueue{public String[]arr = null;public int first;public int last;public MyBlockingQueue(int capacity) {arr = new String[capacity];}public int size;public void put(String s) throws InterruptedException {synchronized (this) {if (size == arr.length) {this.wait();}arr[size] = s;if (size > arr.length) {last = 0;}size++;this.notify();}}public String take() throws InterruptedException {String ret = "";synchronized (this) {if (size == 0) {this.wait();}ret = arr[first];first++;if (first >= arr.length) {first = 0;}size--;this.notify();}return ret;}
}
我们通过观察wait()方法的源代码发现源码建议我们将wait()方法 放在while循环中使用
原因:因为wait并非只能被notify来唤醒,还可能被interrupt方法以打断的方式来唤醒
while的作用就是在wait被唤醒之后再次确认条件,看是否能继续执行
场景:实现一个充值的逻辑,某个线程在阻塞等待,队列里玩家的充值数据,一旦充值数据到账就把对应的道具发放给玩家,可能会出现玩家可能正要充值(还没充)因为interrrupt的不小心操作,导致队列里读取出一个“错误值”
public class Demo5 {private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{synchronized (locker) {System.out.println("wait之前");try {locker.wait();} catch (InterruptedException e) {System.out.println("wait之后");e.printStackTrace();}}});Thread.sleep(1000);t.start();t.interrupt();} }
Interrupt是能够唤醒阻塞的方法.sleep (wait和sleep 都是类似的阻塞的方法)
lnterrupt本来的目的是,让线程终止.唤醒阻塞,只是"终止线程的一个环节"
3.线程池
“池”这种思想本质上就是为了提高程序的效率,我们之前有学过字符串常量池,如果字符串常量池中已经存在相同的字符串,则不会重复创建新的字符串对象,而是直接返回池中已有的字符串对象的引用。
此处的线程池也是类似的思想,最初引入线程的原因就是进程太重了,频繁的销毁进程开销会很大,(只是相比于线程来说开销很大),但随着业务对于性能的要求越来越高,对应的线程创建/销毁的频次就会越来越多,与此同时线程大批量的开销也会变得越来越大,此时就无法不忽略不计了;而线程池就是解决该问题的常见方案
线程池就是把线程提前从系统当中申请好放到一个地方,当后续需要使用线程的时候直接从这个地方来取,而不是从系统中重新申请,当线程用完之后也是还回到刚才这个地方
(1)操作系统的两种状态
内核态(Kernel Mode)
定义与特点:
- 内核态是操作系统运行的一种高级别状态,也被称为内核空间或特权模式。
- 在内核态下,CPU可以执行任何指令,访问任意的数据,包括外围设备(如网卡、硬盘等),并且可以从一个程序切换到另一个程序,占用CPU时不会发生抢占情况。
- 内核态主要负责运行系统、硬件交互,以及提供稳定的环境供应用程序运行。
- 内核态运行的代码不受任何限制,具有最高的权限级别,通常被操作系统本身及其相关模块所使用。
功能与作用:
- 控制计算机的硬件资源,如协调CPU资源、分配内存资源等。
- 提供系统调用接口,允许用户态的程序通过系统调用来访问内核管理的资源。
- 实现保护机制,防止用户进程误操作或恶意破坏系统。
用户态(User Mode)
定义与特点:
- 用户态是操作系统运行的另一种状态,也被称为用户空间或非特权模式。
- 在用户态下,CPU只能访问受限的资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中才能访问这些特权资源。
- 用户态主要用于执行用户程序,提供应用程序运行的空间。
- 用户态运行的代码需要受到CPU的很多检查,不能直接访问内核数据和程序。
功能与作用:
- 执行用户编写的应用程序代码。
- 通过系统调用请求内核态的服务,如文件操作、网络通信等。
- 在用户态下,每个进程都在各自的用户空间中运行,不允许存取其他程序的用户空间,从而保证了系统的安全性和稳定性。
操作系统是由操作系统的内核和操作系统配套的应用程序组成的,内核是操作系统的核心功能部分,负责完成一个操作系统的核心工作,对应的执行很多代码的逻辑都是要用户态的代码和内核态的代码配合完成的(用户态调用内核的api),多种的应用程序也都是由内核统一负责管理和服务的,内核里的工作就可能时非常繁忙的,也就是说用户态提交给内核要做的任务是不可控的(内核可能还会穿插着做其它事)
- 从系统创建线程,这样的逻辑就是调用系统api,由系统内核执行一系列逻辑来完成这个过程
- 直接从线程中取,就相当于整个过程都是纯用户态的代码,是自己可控的,因此更高效
所以我们认为纯用户态的操作比经过内核的操作效率更高
(2)实现线程池
class MyThreadPool {private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();// 通过这个⽅法, 来把任务添加到线程池中. public void submit(Runnable runnable) throws InterruptedException {queue.put(runnable);}// n 表⽰线程池⾥有⼏个线程. // 创建了⼀个固定数量的线程池. public MyThreadPool(int n) {for (int i = 0; i < n; i++) {Thread t = new Thread(() -> {while (true) {try {// 取出任务, 并执⾏Runnable runnable = queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}}
}public class Demo {public static void main(String[] args) throws InterruptedException {MyThreadPool pool = new MyThreadPool(4);for (int i = 0; i < 1000; i++) {pool.submit(new Runnable() {@Overridepublic void run() {// 要执⾏的⼯作 System.out.println(Thread.currentThread().getName() + " hello"}});}}
}
(3)Java标准库提供的线程池
构造方法
注意事项
Java标准库的线程池中把里面的线程分成两类:
- 核心线程(可理解为最少有多少个线程)
- 非核心线程(线程扩容中新增的)
核心线程数+非核心线程数的最大值就是最大线程数
核心线程会始终存在于线程池内部,非核心线程繁忙时会被创建出来,不繁忙空闲时就会把这些线程真正释放掉
BlockingQueue<Runnable> workQueue表示工作队列,线程池的工作过程是典型的“生产者消费者模型”,我们在使用时通过形容submit这样的方法把要执行的任务(Runnable)设定到线程池里,线程池内部的工作线程就负责执行这些任务我们知道构造方法是一个特殊的方法,必须和类名一样,多个版本的构造方法必须是通过“重载”,那我们就可能会遇到以下情况
四种拒绝策略
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class Demo1 {public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool(4);for (int i = 0; i < 100; i++) {int id = i;service.submit(()-> {Thread current = Thread.currentThread();System.out.println(id + current.getName());});}} }
执行这个代码虽然100个任务都执行完毕了,但是整个进程并没有结束,是因为此处的线程池创建出来的线程,默认都是“前台线程”,虽然main线程结束了,但是这些线程池里的前台线程仍然存在
(4)线程池优点
- 降低资源消耗:减少线程的创建和销毁带来的性能开销。
- 提高响应速度:当任务来时可以直接使用,不用等待线程创建
- 可管理性: 进行统一的分配,监控,避免大量的线程间因互相抢占系统资源导致的阻塞现象。
4.定时器
定时器也是软件开发中的⼀个重要组件.类似于一个"闹钟".达到⼀个设定的时间之后,就执行某个指定 好的代码.
定时器是一种实际开发中非常常用的组件.
比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重连.比如一个Map,希望里面的某个key在3s之后过期(自动删除).
类似于这样的场景就需要用到定时器.
(1)标准库中的定时器
- 标准库中提供了⼀个Timer类.Timer类的核心方法为schedule .
- schedule 包含两个参数.第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后 执行(单位为毫秒).
Timer构造方法:
schedule方法:
import java.util.Timer;
import java.util.TimerTask;public class Demo2 {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1");}},3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2");}},2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3");}},1000);System.out.println("程序开始运行");}
}
(2)实现定时器
- 创建类,描述一个要执行的任务是啥.
(任务的内容,任务的时间)- 管理多个任务通过一定的数据结构,把多个任务存起来.
- 有专门的线程,执行这里的任务
考虑线程安全问题:
import java.util.PriorityQueue;
import java.util.Timer;class TimerTask implements Comparable<TimerTask>{private Runnable runnable;private long time;public TimerTask(Runnable runnable,long delay) {this.runnable = runnable;this.time = System.currentTimeMillis() - delay;}public void run() {runnable.run();}public long getTime() {return time;}@Overridepublic int compareTo(TimerTask o) {return (int) (this.time - o.time);}
}
class MyTimer {private Object locker = new Object();private PriorityQueue<TimerTask> queue = new PriorityQueue<>();//要有专门的线程去执行TimerTaskpublic MyTimer() {Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {while (queue.isEmpty()) {locker.wait();}TimerTask current = queue.peek();// 比如, 当前时间是 10:30, 任务时间是 12:00, 不应该执行.// 如果当前时间是 10:30, 任务时间是 10:29, 应该执行if (System.currentTimeMillis() >= current.getTime()) {// 要执行任务current.run();// 把执行过的任务, 从队列中删除.queue.poll();} else {// 先不执行任务locker.wait(current.getTime() - System.currentTimeMillis());// Thread.sleep(current.getTime() - System.currentTimeMillis());}}}} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}public void schedule(Runnable runnable,long delay) {//添加任务synchronized (locker) {TimerTask timerTask = new TimerTask(runnable,delay);queue.offer(timerTask);locker.notify();}}}public class Demo3 {public static void main(String[] args) {MyTimer myTimer = new MyTimer();myTimer.schedule(() -> {System.out.println("hello 3000");}, 3000);myTimer.schedule(() -> {System.out.println("hello 2000");}, 2000);myTimer.schedule(() -> {System.out.println("hello 1000");}, 1000);System.out.println("程序开始执行");}
}