设计模式:即针对某些特定的场景,大佬们设计出来的一些固定套路来供我们使用 单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例. 这一点在很多场景上都需要.比如JDBC中的DataSource实例就只需要一个. 单例模式具体的实现方式有很多.最常见的是"饿汉"和"懒汉"两种. 单例模式前提是"一个进程中",如果是有多个Java进程,自然每个进程中都可以有一个实例了
“饿汉”二字我们可以理解为迫切的意思,在类加载的时候就会创建这个单例的实例 (单个对象)
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);
}
}首次调用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内存中的最新值
如何解决呢?进行加锁操作,安全性问题时发生在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语句就不再发挥作用,之后都是直接返回该对象,后续的代码就只是单纯的读操作,此时之后的每次加锁就是无用功,每次都会触发加锁的操作,虽然之后不会发生线程安全问题,但却因为加锁这一步骤产生了阻塞进而影响到了性能
因此我们需要再次进行判断,当第一次调用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;
}
}此时依然存在问题,我们上篇文章说过开发编译器的大佬们为了提高大家的效率会进行优化,这里就进行了指令重排序,从而引起了线程安全问题,此时为了防止误判,我们依旧加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;
}
}阻塞队列是一种特殊的队列,也遵循“先进先出”的原则 阻塞队列能是一种线程安全的数据结构,(标准库中原有的队列Queue和其子类默认都是线程不安全的)并且具有以下特性:
阻塞队列的一个典型的应用场景就是“生产者消费者模型”,这是一种典型的开发模型
生活场景举例:

服务器之间的通信举例: 我们当下的分布式系统也是如此,并不是一个系统解决所有问题,而是多个系统之间相互调用


此时服务器之间的耦合性就较高,若一个服务器挂了,与其相连的服务器也会是受到较大的影响,所以此时我们就需要引入阻塞队列使用生产者消费者模型来降低耦合

此时AB之间通过阻塞队列,达到了解耦合的效果,但是A和队列,B和队列之间是否又引入了新的耦合呢?答案是没有,我们通常所谈到的阻塞队列是代码中的一个数据结构,由于这个东西太好用,以至于会把这样的数据结构单独封装成一个服务器程序并且在单独的服务器机器上进行部署,此时这样的阻塞队列就叫做“消息队列”(Message Queue,MQ),消息队列是成熟的产品,代码不会频繁修改,代码是稳定的,所以相互之间的逻辑基本一次就顶下来了
通过中间的阻塞队列,可以起到“削峰填谷”的效果,在遇到请求量激增的情况下可以保护下游服务器不会被请求冲垮 消息队列(阻塞队列)服务器通信过程中,也是能起到的削峰填谷

为什么一个服务器收到的请求越多,就可能会挂? 一台服务器就类似于是一台电脑,上面就提供了一些硬件资源(CPU,内存,硬盘,网络带宽等),尽管配置再好,硬件资源也是有限,当服务器每次收到一个请求,处理这个请求的过程中就都需要执行一系列的代码,在执行这些代码的过程中就会需要消耗一定的硬件资源,当这些请求消耗的总的硬件资源超过了机器能提供的上限,那么此时机器就会出现问题(卡死,程序崩溃等)
在请求激增的时候,A为啥不会挂?队列为啥不会挂?反而是B更容易挂呢?? A的角色是一个“网关服务器”,收到客户端的请求再把请求转发给其他服务器,这样服务器里面的代码做的工作比较简单(单纯的数据转发)消耗的硬件资源通常更少,因此在处理同一个请求时,消耗的资源更少,同样的配置下就能支持更多的请求处理, 同理,队列其实也是比较简单的程序,单位请求消耗的硬件资源也是比较少的 B这个服务器是真正工作的服务器,要真正完成一系列的业务逻辑,这一系列的工作代码量非常大,消耗的时间很多,消耗的硬件资源也是更多的
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
//阻塞队列的容积为3
BlockingQueue<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本来的目的是,让线程终止.唤醒阻塞,只是"终止线程的一个环节"
“池”这种思想本质上就是为了提高程序的效率,我们之前有学过字符串常量池,如果字符串常量池中已经存在相同的字符串,则不会重复创建新的字符串对象,而是直接返回池中已有的字符串对象的引用。 此处的线程池也是类似的思想,最初引入线程的原因就是进程太重了,频繁的销毁进程开销会很大,(只是相比于线程来说开销很大),但随着业务对于性能的要求越来越高,对应的线程创建/销毁的频次就会越来越多,与此同时线程大批量的开销也会变得越来越大,此时就无法不忽略不计了;而线程池就是解决该问题的常见方案 线程池就是把线程提前从系统当中申请好放到一个地方,当后续需要使用线程的时候直接从这个地方来取,而不是从系统中重新申请,当线程用完之后也是还回到刚才这个地方
内核态(Kernel Mode) 定义与特点:
功能与作用:
用户态(User Mode) 定义与特点:
功能与作用:
操作系统是由操作系统的内核和操作系统配套的应用程序组成的,内核是操作系统的核心功能部分,负责完成一个操作系统的核心工作,对应的执行很多代码的逻辑都是要用户态的代码和内核态的代码配合完成的(用户态调用内核的api),多种的应用程序也都是由内核统一负责管理和服务的,内核里的工作就可能时非常繁忙的,也就是说用户态提交给内核要做的任务是不可控的(内核可能还会穿插着做其它事)
所以我们认为纯用户态的操作比经过内核的操作效率更高
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() {
@Override
public void run() {
// 要执⾏的⼯作
System.out.println(Thread.currentThread().getName() + " hello"
}
});
}
}
}

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线程结束了,但是这些线程池里的前台线程仍然存在
定时器也是软件开发中的⼀个重要组件.类似于一个"闹钟".达到⼀个设定的时间之后,就执行某个指定 好的代码. 定时器是一种实际开发中非常常用的组件. 比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重连.比如一个Map,希望里面的某个key在3s之后过期(自动删除). 类似于这样的场景就需要用到定时器.
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() {
@Override
public void run() {
System.out.println("1");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("2");
}
},2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("3");
}
},1000);
System.out.println("程序开始运行");
}
}



考虑线程安全问题:


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;
}
@Override
public int compareTo(TimerTask o) {
return (int) (this.time - o.time);
}
}
class MyTimer {
private Object locker = new Object();
private PriorityQueue<TimerTask> queue = new PriorityQueue<>();
//要有专门的线程去执行TimerTask
public 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("程序开始执行");
}
}