1.多线程的代码案例
1.1 单例模式【非常经典的设计模式】
单例 =>单个实例(对象)
有些场景中,希望有的类,只能有一个对象,不能有多个!!!在这样的场景下,就可以使用单例模式了
EG:代码中,很多用于管理数据的对象就应该是"单例"的.
MySQL JDBC DataSource(描述了 mysql 服务器的位置)
需要让编译器帮我们做监督确保这个对象不会出现多个(出现多个的时候直接编译报错) (强制要求)**这样的思想方法,很多地方都会涉及到,
- final 2)interface 3) @Override 4) throws**
1.饿汉模式
这里的创建时机,是在类加载的时候(比较早的时机)
java
// 期望这个类能够有唯一一个实例.
class Singleton {
private static Singleton instance = new Singleton();
// 通过这个方法来获取到刚才的实例.
// 后续如果想使用这个类的实例, 都通过 getInstance 方法来获取.
public static Singleton getInstance() {
return instance;
}
// 把构造方法设置为 私有 . 此时类外面的其他代码, 就无法 new 出这个类的对象了.
private Singleton() { }
}
public class Demo21 {
public static void main(String[] args) {
// 此处又有一个实例了. 这就不是单例了呀.
// Singleton s1 = new Singleton();
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
1.在类的内部,提供一个现成的实例.
2.把构造方法设为 private,避免其他代码能够创建出实例.
通过上述方式,就强制了其他程序员在使用这个类的时候,就不会创建出多个对象了
2.懒汉模式
也是可以使 SingletonLazy 这个类,只有唯一实例.
首次调用 getlnstance 的时候才会真正去创建出实例(如果不调用,就不创建)
java
class SingletonLazy {
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() { }
}
public class Demo22 {
public static void main(String[] args) {
}
}
上述两种写法是否是线程安全???
懒汉不安全
这个时候,实例已经是多个了.违背了单例的要求.(bug)
锁,不是加了就线程安全加的对不对,非常关键1)锁的 {}的范围,是合理的.能够把需要作为整:体的每个部分都囊括进去
2)锁的对象,也得是能够起到合理的锁竞争的效果.
如何保证懒汉模式是线程安全的呢?
一旦代码这么写,后续每次调用 getlnstance,都需要先加锁了但是实际上,懒汉模式, 线程安全问题,只是出现在最开始的时候(对象还没new呢)
一旦对象new出来了,后续多线程调用 getlnstance,就只有读操作, 就不会线程不安全了【画蛇添足】【加锁开销很大】【影响执行效率】
是否有办法,既可以让代码线程安全,又不会对执行效率产生太多影响呢?
在加锁语句的外层,再引入一个 if 条件, 判定一下,看看当前这里的锁,是否要加上,
如果对象已经有了,线程就安全了.此时就可以不加锁了如果对象还没有,存在线程不安全的风险,就需要加锁.第一个 if 用来判定是否需要加锁.
第二个 if 用来判定是否需要 new 对象,
只不过,凑巧这俩条件是一样的写法,
指令重排序
可能会对上述编码产生影响
也是编译器优化.编译器为了执行效率,可能会调整原有代码的执行顺序,调整的前提是保持逻辑不变
可以按照123 来执行
也可以按照132来执行
(1 一定是先执行的)
哪种顺序, 在单线程下执行都是无所谓的~~
但是在多线程下,就可能有问题了!!!
假设是按照 132 来执行
当 t1 执行完1和3 的时候
此时Instance 就已经非空了!!!
但是,此时 Instance 指向的是一个还没初始化的非法对象
此时此刻,还没执行 2 呢,t2 线程开始执行了!!!
t2 判定 Instance == null ,条件不成立!!! 于是 t2 现成直接 return Instance。
进一步的 t2 线程的代码就可能会访问 Instance 里面的属性和方法了
这就会导致bug
仍有一个问题
针对上述问题,解决方案,仍然是 volatile
让 volatile 修饰 Instance.此时就可以保证 Instance 在修改的过程中就不会出现指令重排序的现象了
2.阻塞式队列
2.1 阻塞队列是什么
阻塞队列是一种特殊的队列. 也遵守 " 先进先出 " 的原则 .
阻塞队列能是一种线程安全的数据结构 , 并且具有以下特性 :
1.线程安全
2.带有阻塞特性
a)如果队列为空,继续出队列,就会发生阻塞,阻塞到其他线程往队列里添加元素为止
b) 如果队列为满,继续入队列,也会发生阻塞,阻塞到其他线程从队列中取走元素为止
阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型.
生产者,把生产出来的内容,放到阻塞队列中,
消费者,就会从阻塞队列中获取内容
2.2 生产者消费者模型的意义
1.解耦合
之前:
如果 A 和 B 直接交互.(A 把请求发给 B,B 把响应返回给 A)彼此之间的耦合就是比较高的.
1)如果 B 出现问题,很可能就把 A 也影响到了
2)如果未来再添加一个℃,就需要对 A 这边的代码,做出一定的改动,
之后:
耦合就会被降低~~如果 B 这边出现问题,就不会对 A 产生直接的影响(A 只是和队列交互,不知道 B的存在)
后续如果新增一个 C,此时,A 不必进行任何修改只需要让 C 从队列中获取数据即可.
2. 削峰填谷
削峰:短时间内,请求量比较多
填谷:请求量比较少
比如 B 要操作数据库,数据库本身就是一个分布式系统中,相对脆弱的环节
引入生产者消费者模型会得到极大的改善。
A 这边收到了较大的请求量,A 会把对应的请求写入到队列中。
B仍然可以按照之前的节奏,来处理请求。
比如,正常情况下,A 和 B,每秒钟处理 1000 次请求极端情况下,A 这边每秒要处理 3000 次请求,如果让 B也处理 3000次, 就要挂了队列帮 B 承担了压力.B 仍然可以按照 1000次的节奏,处理请求.
与其直接把 B 搞挂了,不如让 B 慢点搞.虽然 A 这边得到响应的速度会慢,总好过完全没响应就会有一定的请求在队列中积压.
像上述的峰值情况,一般不会持续存在,只会短时间出现. 过了峰值之后,A 的请求量就恢复正常了B 就可以逐渐的把积压的数据都给处理掉了
有了这样的机制之后,就可以保证在突发情况来临的时候,整个服务器系统仍然可以正确执行
2.3 阻塞队列的使用
在 Java 标准库里,已经提供了现成的 阻塞队列,让咱们直接使用
标准库里, 针对 BlockingQueue 提供了两种最要的实现方式:
1.基于数组
2.基于链表
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
Queue 这里提供的各种方法, 对于 BlockingQueue 来说也可以使用.
但是一般不建议使用这些方法.这些方法,都不具备"阻塞"特性。
put 阻塞式的入队列
take 阻塞式的出队列
2.4 阻塞队列的实现
了解标准库的阻塞队列怎么用,固然是一个环节,更重要的,是我们能够自己实现出一个阻塞队列.
基于一个普通的队列,加上线程安全,加上阻塞就可以了
普通队列, 可以基于数组,也可以基于链表
存在一个问题:
初始情况下,队列为空的,head 和 tai 重合的.
当队列满了的时候~~
head 和 tail 又重合了
【解决方案】
1.浪费一个格子,让 tail 指向 head 的前一个位置, 就算满了
2.专门搞一个变量, size,来表示元素个数.size 为0 就是 空, 为 数组最大值, 就是 满.
1.记得枷锁,原子性得到很好的保证
java
// 不写作泛型了, 直接让这个队列里面存储字符串.
class MyBlockingQueue {
// 此处这里的最大长度, 也可以指定构造方法, 由构造方法的参数来制定.
private String[] data = new String[1000];
// 队列的起始位置.
private volatile int head = 0;
// 队列的结束位置的下一个位置.
private volatile int tail = 0;
// 队列中有效元素的个数.
private volatile int size = 0;
// private final Object locker = new Object();
// 提供核心方法, 入队列和出队列.
public void put(String elem) throws InterruptedException {
synchronized (this) {
while (size == data.length) {
// 队列满了.
// 如果是队列满, 继续插入元素, 就会阻塞.
this.wait();
}
// 队列没满, 真正的往里面添加元素
data[tail] = elem;
tail++;
// 如果 tail 自增之后, 到达了数组末尾. 这个时候就需要让它回到开头 (环形队列)
if (tail == data.length) {
tail = 0;
}
size++;
// 这个 notify 用来唤醒 take 中的 wait
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this) {
while (size == 0) {
// 队列空了.
this.wait();
}
// 队列不空, 就可以把队首元素 (head 位置的元素) 删除掉, 并进行返回.
String ret = data[head];
head++;
if (head == data.length) {
head = 0;
}
size--;
// 这个 notify 用来唤醒 put 中的 wait
this.notify();
return ret;
}
}
}
public class Demo24 {
public static void main(String[] args) {
// 生产者, 消费者, 分别使用一个线程表示. (也可以使用多个线程)
MyBlockingQueue queue = new MyBlockingQueue();
// 消费者
Thread t1 = new Thread(() -> {
while (true) {
try {
String result = queue.take();
System.out.println("消费元素: " + result);
// 暂时先不 sleep
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 生产者
Thread t2 = new Thread(() -> {
int num = 1;
while (true) {
try {
queue.put(num + "");
System.out.println("生产元素: " + num);
num++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
一个队列,要么是空,要么是满
take 和 put 只有一边能阻塞.
如果 put 阻塞了,其他线程继续调用 put 也都会阻塞. 只有靠 take 唤醒如果 take 阻塞了,其他线程继续调用 take 也还是会阻塞, 只有靠 put 唤醒
所以, 使用 wait 的时候, 一定要注意!!!
考虑当前 wait 唤醒, 是通过 notify 唤醒(说明其他线程调用了 take,此时队列已经不满了,可以继续添加元素),还是通过 Interrupt 唤醒(此时队列其实还是满着的,继续添加元素,肯定会出问题!!)