补充:synchron(锁对象):给对象里面做了一个标记,每个对象,除了代码中写的属性外,此外还有一部分空间,存储的是标志位,这个标志位相当于是加锁,当这一位被标记加锁之后,此时其他线程也想对这个对象标识,就会进行阻塞等待。
面试小技巧:某某hr问,有没有女朋友,闭眼睛猛猛答没有女盆友,拒绝奇奇怪怪的送命题
给你50w啥的你打算干什么——公司附近买房 🌝 🌝 🌝
(下面的两个解决线程安全性这块还会有一部分技巧,请坚持看完,并尽量转发给你的朋友)
一、💛
初步介绍,面试的两个重点
1.单例模式:一种设计模式(设计模式:介绍了很多典型场景的处理方式,按模式写代码,不会写的很烂)
有时候希望对象,在整个线程中只实例化一次(也就是只new一次)(那么如何保证只new一次呢,靠程序猿们的嘴咩🌚🌚,男人的话不可信,所以是让编译器,去进行更严格的检查)
2.工厂模式(以后会写)
二、 💙
单例模式的细分:
1.饿汉模式(迫切):程序启动,我就立马new个床照实例。
2.懒汉模式(延时):第一次使用实例的时候,再创建,否则能不建立就不建立。
两个哪个好,也是分情况
假如一共整个大文件40G
饿汉是会一次尝试吧所有内容加载完毕,再显示出来(半小时体验卡)
懒汉是只加载一部分内容,其他部分,用户翻页用到了再去加载。
三、💜
饿汉的代码实现(一键缓存)
获取实例一定就是上面的那个实例,但是一定不能创建对象吗,其实也不是,反射可以去操作来创新(反射如同小门一样,但是不推荐用)
import java.util.Scanner;
class Singleton{
private static Singleton instance=new Singleton(); //让他变成类属性
public static Singleton getInstance(){ //类方法,这俩个都是类被加载
return instance; 就开始使用
}
private Singleton(){} //可以让他不再new对象,要是new就报错
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
System.out.println(Singleton.getInstance());
}
}
懒汉模式。 他需要首次调用getInstance()才会new,也就是一定是比饿汉慢的
import java.util.Scanner;
class SingletonLAZY{
private static SingletonLAZY instance;
public static SingletonLAZY getInstance(){ //不去调用getInstance这个方法
if(instance==null){ 就不去去new这个对象
instance=new SingletonLAZY();
}
return instance;
}
private SingletonLAZY(){} //不让你去new对象,否则报错
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
System.out.println(s1==s2);
}
}
四、❤️
(情况1)多线程的线程安全,多个线程调用setInsetance(务必会读(辛可肉耐子)会写)情况下,哪个代码是线程安全的
1.饿汉模式,多线程下读取instance内容,多线程读取同一个变量没有问题
2.懒汉:懒汉就会出现些问题,我那个上面的判断是否是空,然后再new个对象,但是他并不是原子性的啊(不知道啥原子性,看我前面的博客哦家人。)那么多线程情况下就会如下图所示. xx-instance. yy-Singletion
当然某某盆友说,第二次的操作,不就是把原来的instance的引用给修改了,之前new出来的对象不是立刻就被JVM的gc回收了吗(垃圾回收机制)。最后不还是只剩一个对象了咩?
但实际上new 一个对象可能开销非常大!可能服务器启动100G内存中,都是由一个对象管理的,可能new一个花费20分钟
那么我们该如何解决呢?
首先和我们之前的处理方式一样,把if和new放到一个整体,
class Singleton{ private static Singleton instance; public static Singleton getInstance() { synchronized (instance) { //把他们两个加锁 if (instance == null) { instance = new Singleton(); } return instance; } } private Singleton(){} }
这也就意味着:刚才发的图的情况,会进行阻塞,也就让这个线程没有了上述的bug。
加锁成本高,可能引起阻塞等待,无脑加锁会导致程序执行效率受到影响。
(vector,HashTable,StringBuffer,也不是很常用,他们的关键方法都用了synchronized,无论单、多线程,是否有线程安全,都会有🔒,也会影响效率)
(情况1号改进衔接2号)但是像这个代码一样,每次都调用getInstance都要加锁,但是这样有必要吗?
线程不安全,主要是首次new对象才有问题,一旦对象new成功了,后续调用getInstance都是安全的。
改进:
class Singleton{ private static Singleton instance; public static Singleton getInstance() { if(instance==null){ //先判断是否需要创建对象 synchronized (instance) { //下面这两个才可能是线程安全的主要问题,加锁 if (instance == null) { instance = new Singleton(); } } } return instance; //不需要就,直接返回,没有什么线程安全问题 } private Singleton(){} }
有人可能说同一个条件,写两遍合理吗?
之前是没有阻塞,但现在多线程,第二个条件和一个条件之间间隔非常长的时间,这个间隔,可能别的线程就把instance改变了
(情况1号衔接2号同时考虑3号情况)
两个线程
第一个线程修改完instance之后,就结束了代码块,释放了锁
第二个线程能够从中获取到锁从阻塞中恢复了
但是if(instance==null),t2的读操作一定会读到1线程修改的值吗?内存可见性问题,此处就需要给instance加上volatile(会读会写 (wao(平🐍)里太哦)
这样是更加稳妥的,只有这样才可以避免风险,毕竟编译器优化还是有点离谱的,是否会去触发优化,不好说,但是加上volatile更稳妥
并且volatile还有另外的用途,避免此处赋值操作进行指令重排序
如果单线程则不会出现指令重排序问题,但是多线程可能就会出现问题了(也是编译器优化的一种手段,保证原有执行的逻辑程序的情况下,对代码执行效率顺序调整,使执行效率提高。
instance=new SingletonLazy()分成3步
1.把对象创建出内存空间,得到内存地址
2.把空间调用构造方法,对对象进行初始化。
3.把内存地址,复制给instance重引用。
这三步可能进行重排序->单个线程先2后3,先后无所谓
但是假如t1先进行第一步,在进程第三步,第二步还没做的时候,出现了线程切换,此时还没给对象初始化就调度到其他线程了。这样t2在判读是否为空,直接返回instance,并且后续假如一直使用instance的一些属性或者方法,instance是没有被初始化啊,不可靠,不能随便用,否则什么🐮🐎情况都蹦出来了。
用volatile(保证多线程环境下的可见性,有序性)一定按123执行,此时不让他这么优化,也就不会产生重排序。
1️⃣加锁
2️⃣双层if
3️⃣volatile
面试小技巧(全是套路):考官让写这个懒汉饿汉什么的(装思考一会🤔),别一下把全优化的写上去,先装傻,写那个不完全的版本,然后他在说如何如何,再去一步一步慢慢改,让他觉得这个考题,不是你提前准备的,然后他就会觉得,小伙脑子好使,灵光
五、💚
阻塞队列(有阻塞功能的队列,下一章或者下两章是如何实现)
1.当队列满的时候,继续入队列,就会出现阻塞,阻塞到其他线程从队列中取起元素为止。
2.当队列空的时候,继续出队列,也会出现阻塞,阻塞到其他线程往队列添加元素为止
用处非常大,也很重点
通过阻塞队列可以实现生产者,消费者模型
如包饺子
生产者:整饺子皮
消费者:包饺子
放饺子的桌子就是交易场所。
六、 💓
生产者,消费者模型优势
1.解耦合(两个关系块的关系叫耦合):降低模块之间的联系
改进引入阻塞队列:
此时,A和B就通过阻塞队列很好的解耦合了~此时如果A或者B挂了,由于他们彼此之间,不会直接交互,没有啥太大影响,如果新增服务器C,此时A服务器完全不需要任何修改,只需要让他从队列中取元素就可以。
2.削峰填谷:服务器收到来自客户端/用户的请求,不是一成不变的,可能因为一些突发事件,引起请求数目暴增。(如某某明星恋情,干翻微博),正常情况下同一时刻请求的数量都是有限的。
一个分布式系统,有的机器承担压力大,有的机器承担压力小
此时A收到一个请求,B也需要立刻去处理这一个请求,如果A能承受的压力大,B能承受的压力小的话,此时,很可能B就先挂了
但假如说用上,上面的阻塞队列
削峰:当外界的请求突然暴涨,A收到的请求多了,A就会给队列中写入更多的请求数据,但是B可以依旧按照自己的节奏处理,B不至于挂了,相当于队列起到了缓冲的作用。
填谷:峰值多的时候只是暂时的~当峰值消退的时候,A收到的请求少了,B还按照原有速率去处理,不至于他特别空闲(填谷)
七、💗
JAVA提供了现成的阻塞队列
BlockingDeque:阻塞队列提供的接口,需要具体实例化类
BlockingQueue<String>queue=new ArrayBlockingQueue<>(3);//基于数组(最好提前知道他多少个元素,避免频繁扩容) BlockingQueue<String>queue2=new PriorityBlockingQueue<>(3);//基于优先级队列(就是堆),有优先级就选它BlockingDeque<String>queue1=new LinkedBlockingDeque<>(3);;//基于链表(不知道多少个元素使用链表,因为要修改put tabke带阻塞功能
offer poll不带阻塞功能,
基于这个写个生产者消费者模型
import java.util.Scanner; import java.util.concurrent.*; public class Demo { public static void main(String[] args) { BlockingDeque<Integer>queue1=new LinkedBlockingDeque<>(); Thread t1=new Thread(()->{ int count =0; while (true){ try { queue1.put(count); //t1把生产的数字放入队列中 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("生产了元素"+count); count++; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2=new Thread(()->{ while(true) { Integer n = null; try { n = queue1.take(); //t2接受他们t1线程往队列里填的数 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者" + n); } }); t1.start(); t2.start(); } }