文章目录
- 多线程设计模式
- 什么是设计模式
- 单例模式
- 饿汉模式
- 懒汉模式
- 线程安全问题
- 懒汉模式就一定安全吗?
- 锁引发的效率问题
- jvm的优化引起的安全问题
- 阻塞队列
- 阻塞队列是什么?
- 生产消费者模型
- 阻塞队列实现消费生产者模型可能遇到的异常
多线程设计模式
什么是设计模式
首先我们要先明白什么是设计模式呢?举个栗子,设计模式就像我们下棋的棋谱一样按照某种需求按照一定的规则来进行特定的应对软件开发中也有很多情景。因此大佬们总结了一套经典的设计模式其中面试经常问的当然就是单例模式了
单例模式
什么是单列模式呢?单列模式字面意思我们拆开来看
什么时单呢?单就是单一,一个的意思。列是什么呢?就是实例。合起来就是一个实例,也就是说这个类只能实列化出一个对象,那么该怎么实现这样的方式呢?其实很简单我们只需要把构造方法搞成私有的就可以了那么代码如下
class Mytest{
private static Mytest mytest=new Mytest();
private Mytest(){
}
private Mytest getMytest(){
return mytest;
}
}
public class Main {
public static void main(String[] args) {
}
}
这样子我们就可以做到不能自己创建对象而只能通过getMytest()获取已经创建好的对象。那么这时候就涉及到两种模式了就是饿汉模式和懒汉模式
饿汉模式
饿汉模式是什么呢?其实就是我们上面的那种代码,就是即使我们现在还没有调用这个类还不需要这个类的对象我们都已经把他实列化出来了一个对象了这就是饿汉模式。也就是当我们即使没用到这个实例的对象也先把对象创建好就像一个饿汉一样扑到饭上。
懒汉模式
说完了饿汉模式我们来讲一下懒汉模式,什么是懒汉模式呢?我们对比一下饿汉的概念来类比,懒汉就是当我们需要这个类的对象的 时候再给我们实列化出来代码如下
class Mytest{
private static Mytest mytest;
private Mytest(){
}
private Mytest getMytest(){
if(mytest==null){
mytest=new Mytest();
}
return mytest;
}
}
public class Main {
public static void main(String[] args) {
}
}
代码就是像上面这样,先判断一下对象是否被创建,如果没有被创建那么就实例化处对象并将对象返回如果已经创建的话那就把创建好的对象直接返回让其使用。
线程安全问题
那么讲到这里我们来思考一下,懒汉模式和饿汉模式哪个是线程安全的呢?其实懒汉模式是线程安全的,因为我们可以看一下饿汉模式代码如下
class Mytest{
private static Mytest mytest=new Mytest();
private Mytest(){}
public static Mytest getMytest(){
return mytest;
}
}
public class Main {
public static void main(String[] args) {
Thread t1=new Thread(()->{
Mytest ty=Mytest.getMytest();
});
Thread t2=new Thread(()->{
Mytest ty2=Mytest.getMytest();
});
t1.start();
t2.start();
}
}
当我们使用饿汉模式的时候我们两个线程在分别调用Mytest的时候就会导致我们两个线程创建的mytest是不一样的,我们要明白一件事情就是当一个资源被多个线程即读取又修改的时候那么它多半其实就是不安全的。当一个资源只是被读取的时候那么它也就是安全的。这时候我们再来看懒汉模式就会发现我们加了一个if就会使得当我们第一次创建好这个对象之后后续的线程是无法更改这个对象的,因此他就是线程安全的。
懒汉模式就一定安全吗?
可是我们要知道一个事情就是懒汉模式就一定安全吗?其实不是的,我们上面说的只是相对安全而已。那么为什么懒汉也是不安全的呢?其实是因为我们创建对象的过程他不是一个原子性的过程他是分成了几个步骤的
new对象的步骤分为三步:
- 分配内存
- 构造对象
- 赋值给对象引用
那么当我们执行这三步的时候其实就会有之前跟++类似的过程,我们画图来解释一下。
我们来举个例子帮助大家更好的了解一下。
那么这时候有什么办法可以解决这个不稳定因素呢?很简单就是加锁就可以了。代码如下
class Mytest{
public static Object ob=new Object();
private static Mytest mytest=null;
private Mytest(){}
public static Mytest getMytest(){
synchronized (ob){
if(mytest==null){
mytest=new Mytest();
}
}
return mytest;
}
}
public class Main {
public static void main(String[] args) {
Thread t1=new Thread(()->{
Mytest ty=Mytest.getMytest();
});
Thread t2=new Thread(()->{
Mytest ty2=Mytest.getMytest();
});
t1.start();
t2.start();
}
}
那么加锁后上面的过程就变成了下面这样。
class Mytest{
public static Object ob=new Object();
private static Mytest mytest=null;
private Mytest(){}
public static Mytest getMytest(){
synchronized (ob){
if(mytest==null){
mytest=new Mytest();
}
}
return mytest;
}
}
public class Main {
public static void main(String[] args) {
Thread t1=new Thread(()->{
Mytest ty=Mytest.getMytest();
});
Thread t2=new Thread(()->{
Mytest ty2=Mytest.getMytest();
});
System.out.println(e);
t2.start();
}
}
那么这时候我们的代码就做到了线程安全,可是还有一个问题就是效率问题
锁引发的效率问题
这时候我们再来思考一下这个代码的进程。首先t1线程获取锁,然后开始创建对象,t2线程在t1线程还没有结束之前就无法获取到这把锁那么这时候就需要去等待,可是这时候就有一个问题那就是说假如我们有100个线程都需要使用这个对象那么都需要先判断一个这个对象是否被创建那么这时候就需要轮着去申请锁释放锁,我们要知道一个事情那就是申请锁释放锁这个过程是非常消耗时间的,因此如果一个代码涉及到多次对锁的释放和申请的话那么这个代码注定与高效率无缘了。那么该怎么办去改善效率问题呢?很简单我们只需要再加一个if就可以了
class Mytest{
public static Object ob=new Object();
private static Mytest mytest=null;
private Mytest(){}
public static Mytest getMytest(){
if(mytest==null){
synchronized (ob){
if(mytest==null){
mytest=new Mytest();
}
}
}
return mytest;
}
}
public class Main {
public static void main(String[] args) {
Thread t1=new Thread(()->{
Mytest ty=Mytest.getMytest();
});
Thread t2=new Thread(()->{
Mytest ty2=Mytest.getMytest();
});
System.out.println(e);
t2.start();
}
}
那么就有人有疑问了因为刚刚说过我们的new不是一个原子性的操作如果说我们第一个线程在创建对象的期间那么这个对象的引用就还是空的这时候其余的线程还是可以通过第一个if的然后那不还是需要去等待锁释放锁吗?所以加个if有什么用呢?其实很有这是很有道理的,但是大家可以想一下这是不是只存在这个对象还没被创建的时期,如果这个对象已经被创建的话那么其余的线程就无法再去获取这把锁了我们避免的是当对象已经创建好后,后续线程想要调用这个引用还需要去获取锁的这种情况。
也就是下面的这个过程
jvm的优化引起的安全问题
在我们多线程创建的过程中我们上面提到了一个事情就是其实new的过程并不是原子的过程,而这其中呢jvm是有优化的也就是说正常来说我们的 步骤应该是
- 分配内存
- 构造对象
- 赋值给对象引用
但是由于线程的优化导致我们的过程可能就变成了 1 3 2,也就是
- 分配内存
- 赋值给对象引用
- 构造对象
然后当一个对象执行到赋值给对象引用的时候那么这时候我们代码中的mytest就已经不是空的了。也就是说这时候就有可能导致我们的if循环不会进去阻塞而是把还没有完全创建好的对象直接给我们返回比如下面的这个示意图
那么这时候该怎么解决呢?那就是加一个volatile
修改后的代码如下
class Mytest{
public static volatile Object ob=new Object();
private static Mytest mytest=null;
private Mytest(){}
public static Mytest getMytest(){
if(mytest==null){
synchronized (ob){
if(mytest==null){
mytest=new Mytest();
}
}
}
return mytest;
}
}
public class Main {
public static void main(String[] args) {
Thread t1=new Thread(()->{
Mytest ty=Mytest.getMytest();
});
Thread t2=new Thread(()->{
Mytest ty2=Mytest.getMytest();
});
System.out.println(e);
t2.start();
}
}
阻塞队列
阻塞队列是什么?
首先我们要先明白阻塞队列是什么呢?阻塞队列其实就是一种特殊的队列他也是按照先进先出的顺序的,但是他跟普通队列的区别是什么呢?其实就是线程的安全性,当我们学到了多线程后我们就要明白,一个队列在未来可能不只是一个线程再往里面填充元素,也不一定是一个线程再往里面移除元素,因此线程安全性就很重要了。那么它的特点就体现在以下方面
- 当队列满了的时候放入元素就会堵塞
- 当队列空的时候移除元素就会堵塞
- 当队列放入元素正在阻塞的时候移除一个元素可以解除其放入元素堵塞的情况
- 当对列移除元素为空的时候添加一个元素就可以解除其移除元素堵塞的情况。
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
生产消费者模型
什么是生产消费者模型呢?我们用阻塞队列为列将两者结合起来进行讲解,我们可以把阻塞队列看成一个钱包那么这时候有两个线程。生产线程和消费线程。
那么这就是一个生产消费者模型,生产线程负责往里放元素,消费线程负责往里取出元素也就是在消费。
那么当时说的阻塞到底是怎么实现的呢我们来看一下下面的这个代码。
import java.util.concurrent.BlockingQueue;
public class MyBlockQueue {
public String[] BlockQueue=new String[100];
private int tail=0;
private int head=0;
int size=0;
public void put(String elem){
synchronized (this){
if(size==BlockQueue.length){
try {
this.wait();
} catch (InterruptedException e) {
//throw new RuntimeException(e);
}
}
BlockQueue[tail++]=elem;
if(tail==BlockQueue.length){
tail=0;
}
size++;
}
}
public String take(){
synchronized (this){
if(size==0){
return null;
}
String ret=BlockQueue[head];
head++;
if(head==BlockQueue.length){
head=0;
}
size--;
this.notify();
return ret;
}
}
}
那么现在我们来解读以下这个代码这个代码中呢假如了锁,具体的意思就是说当我们put的时候假如说我们的这个队列已经满了的话那么我们这时候生产线程就会陷入等待直到我们的消费线程将这个元素取出来之后,才会将其唤醒从而继续执行但是这里面我们为什么要进行抛出异常呢?
阻塞队列实现消费生产者模型可能遇到的异常
这里面为什么我们要加上抛出异常呢?因为我们要知道一个事情那就是唤醒线程不止是notify可以唤醒还有一种唤醒方式那就是intrrupt。当我们的intrrupt方法唤醒线程的时候就会导致一个问题那就是我们的出现bug,因为我们的阻塞队列是模拟的循环队列进行的因此当队列满了之后却不通过正确的途径去将其启动的话,就会导致我们的前面插入的元素被后面插入的元素覆盖掉因此这时候就需要我们进行一些手段来预防,那么该怎么办呢?其实interrupt进行线程启动的时候是会导致抛出异常的我们只需要对异常进行捕获就可以了那么代码如下
public class Main {
public static void main(String[] args) {
MyBlockQueue mytest=new MyBlockQueue();
Thread t1=new Thread(()->{
int num=0;
while(true){
mytest.put("生产者生产了"+num+"元素");
System.out.println("生产者生产了" + num + "元素");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
num++;
}
});
Thread t2=new Thread(()->{
int num=0;
while(true){
String ret=mytest.take();
System.out.println("消费者消费了这个"+ret+"元素");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
//t2.start();
t1.interrupt();
}
}
上面的代码确实可以解决这样的一个问题可是在实际开发中我们会感觉这样子是不是太粗暴了毕竟我只是操作失误但是却直接抛出异常,代码终止如果我们不希望这么暴力怎么办呢?其实很简单只需要在唤醒的之后再加个if就可以了如下图
但是这样就可以了吗当然不是这时候是两个线程假如说是有多个线程呢?那么该怎么办难道无限if套下去?当然不是,我们可以加个while循环啊
这里解释以下wait的异常我们还是需要捕获的但是可以不做处理
我们的运行截图就变成了
这个样子也就是当我们的长度到达了我们设置的长度之后就停止运行了。
像这样那么我们的代码就变成了下面这样
import java.util.concurrent.BlockingQueue;
public class MyBlockQueue {
public String[] BlockQueue=new String[100];
private int tail=0;
private int head=0;
int size=0;
public void put(String elem){
synchronized (this){
while(size==BlockQueue.length){
try {
this.wait();
} catch (InterruptedException e) {
}
}
BlockQueue[tail++]=elem;
if(tail==BlockQueue.length){
tail=0;
}
size++;
}
}
public String take(){
synchronized (this){
if(size==0){
return null;
}
String ret=BlockQueue[head];
head++;
if(head==BlockQueue.length){
head=0;
}
size--;
this.notify();
return ret;
}
}
}
爱人是这个寒冷的世界上的一束温暖的阳光。