Java EE:2.多线程-初阶(第六弹):多线程案例-单例模式
目录
Q2:在new的时候会自动调用无参的构造方法,所以把它设置为private?
答疑:咱之前讲的时候不是说=这个赋值操作是原子的呀,+=、-=这些才是非原子的呀
书接上文:Java EE:2.多线程-初阶(第五弹)~
阶段性小结
内存可见性=>volatile=>JMM
等待通知 wait/notify→Object提供的方法
也可以通过wait/notify来解决线程饿死问题
锁对象得和调用wait/notify的对象是一致的
使用一次notify唤醒多个线程的wait,随机唤醒其中一个线程
如果想唤醒所有,需要调用多次notify
也可以使用notifyAll
需要确保已经wait了,再notify
8.多线程案例
关于多线程的基本知识性的概念都讲完了,“光说不练假把式”,接下来,就通过多线程案例来实操一下~
8.1单例模式
单例模式=>是一种设计模式
答疑:设计模式是instance吗?
instance是实例,我们写代码new的一个对象,就可以叫做Instance,单例模式,使用了Instance这样的概念,但是,不等于Instance
设计模式≈棋谱
闲聊:
有些同学擅长象棋围棋,有的在象棋比赛中拿过市级冠军,还有同学,是五子棋国家二级运动员……
在我们投程序员岗位的时候,这样的棋类奖项,是否要写到简历上呢??
要写的,还很加分!能在棋类方面拿奖,说明你智商很高~~
写代码,虽然不要求你智商非得达到120、130之类的,但肯定是公司乐意招智商高的人~~
那篮球比赛/足球比赛/羽毛球比赛……这类纯体力相关的奖项,是否要写简历上??
也可以写!说明你体力好,不怕加班~身体素质>智力水平
棋谱:大佬把一些对局整个推演过程,写出来~~“当头炮,马来跳”~~
只要你照着棋谱来下棋,不说你这个棋下的多好,但是一定不会太差~(提高棋手的下限)
设计模式,属于是程序员的棋谱~~
大佬们,把一些典型的问题场景,整理出来,并且针对这些场景,代码该怎么写,具体方案给出了一些指导和建议~~
程序员掌握了设计模式,咱们这个代码再怎么写,也不会太差~~
答疑:
Q1:套公式吗?
对对对!菜鸡程序员只要按照设计模式的方式来写代码就好了,而大佬们要考虑的事儿就多了~~
Q2:和框架差不多了
二者目标是一致的~~
框架是属于“硬性要求”
设计模式属于“软性要求”,只是一个建议,也可以不这么写
Q3:设计模式是23种吧?
设计模式,有很多种,不是只有23种~~
简单解释一下为啥是23~
之前有三位大佬,写了一本书“设计模式”
这本书影响力非常大,主要就是讨论了23种设计模式~~
但是后续设计模式也在不断演化
不同的编程语言,由于语法风格不同,有些设计模式可能在某个语言中是不适用的~~
单例模式:设计模式中一种非常典型的模式,也是比较简单的模式,还是校招中最容易被考到的设计模式~
”单例“模式,就是只有单个实例(对象):强制要求,某个类,在某个程序中,只有唯一一个实例(不允许创建多个实例,不允许new多次)
class Teat{ …… } Test t=new Test();//这就是对象/实例~闲聊:
在编程中,实例这个术语,往往有多种含义,实例的应用场景比对象更广泛~~
一台服务器,也能称为是一个实例
一个服务器中运行的一个应用程序,也能称为是一个实例……
只有一个实例,这样的要求,开发中,是很常见的需求场景~
拿MySQL的JDBC举例:JDBC代码的基本流程👇
1.创建DataSource=>描述了数据库服务器在哪里(url,user,password)
2.建立连接,dataSource.getConnection();
3.拼装SQL语句:Statement或者PreparedStatement
4.执行SQL:execute方法/executeQuery/executeUpdate
5.遍历结果集合:ResultSet、迭代器遍历
6.关闭资源
1)ResultSet
2)Statement
3)Connection
其中第一条的“DataSource”就非常时候于作为单例~~,描述数据库的信息的
因为类似于存储数据库信息这样的对象,由于数据库只有一份,即使搞多个这样的对象,也没啥意义~~(就算搞出多个对象,也是一样的信息)
虽然日常开发,不一定真的用JDBC,但是JDBC是一切Java中数据库框架的基础~~
大家可能把JDBC都忘了~再举一个例子吧👇
当时的广告服务器启动的时候,需要加载很多数据(很多个hash表)到内存中,这个过程,查数据库不行(太慢)
当时代码中有一个专门的类DataCenter组织这些数据,此处这个DataCenter这个类也就需要单例的,一个实例就管理了100G的数据(当时服务器也就256G内存)
单例模式,强制要求一个类不能创建多个对象
不是一个口头上的“君子协定”(我就创建一个,保证后续不再创建~~)
必须通过机器/程序,强制要求~~
答疑:通过eslint的静态检查?
是种方案,但不够强制,lint属于是静态检查,一个并列于编译器的工具,并不是真的把代码进行编译,而是把代码里面的内容通过扫描的方式分析一下是否有不科学的代码(属于第三方工具,要求不够严格)
而我们程序员希望的是:在代码中,如果创建了多个实例,直接编译失败~
单例模式具体的实现方式有很多,最常见的就是“饿汉模式”和“懒汉模式”两种👇
饿汉模式
class Singleton{//饿汉模式 //用静态引用指向我们创建的实例,既然是单例模式,必然有个单例,这个单例就在这里创建 private static Singleton instance=new Singleton(); //用get方法获取到我们创建的实例 //后续统一通过getInstance方法获取这里的实例 public static Singleton getInstance(){ return instance; } //单例模式的“点睛之笔”,私有化之后,在类外面进行new操作,都会编译失败~~ private Singleton(){ } }其中Singleton就是单例的意思
答疑:代码里没有字段??
不是没有字段,而是可以根据需要,往这里添加任何你需要的字段~~
new Singleton();=>静态成员的初始化,是在类加载的阶段触发的,类加载往往就是在程序一启动就会触发
饿汉:饿了很久,就会迫切的想去吃,类似的,我们需要用到这么个实例,什么时候创建实例?也是非常迫切,程序一启动,就去创建实例了~
我们可以通过上述代码,判断两个引用t1和t2是不是一样的值👇
package thread; //通过饿汉模式构建单例模式 class Singleton{ private static Singleton instance=new Singleton(); public static Singleton getInstance(){ return instance; } private Singleton(){ } } public class Demo27 { public static void main(String[] args) { Singleton t1=Singleton.getInstance(); Singleton t2=Singleton.getInstance(); System.out.println(t1==t2); } }打印结果说明它们确实指向同一个对象👇
此时我们再去新创建一个实例,会发现报错👇
答疑:
Q1:静态成员不用一定private修饰吧?
当然可以不用,但我们一般习惯上属性都是用private修饰的~~
Q2:在new的时候会自动调用无参的构造方法,所以把它设置为private?
new的时候,肯定是需要调用构造方法,至于是调用几个参数的构造方法,都可以~~
只要保证不管通过什么方式,外面的方法都不能通过构造方法new出来这个实例来就可以了,因此我们直接把所有构造方法都设成private即可
懒汉模式
懒 和 饿 是相对的:
饿:尽早创建实例
懒:尽量晚的创建实例(甚至可能都不创建了→延迟创建)闲聊:懒,在计算机中,是 褒义词!!!
懒的另一个含义:高效率~~
比如说,结婚之后,男友,吃完饭就把碗洗了,这顿饭用了4个碗,就会立即洗这4个碗
女友,吃完饭,就先把碗放那~~美其名曰“休息一会再洗”,结果就是等到下一顿饭要吃了,发现,碗还没洗,就得先洗碗,再吃饭~~
这顿饭用4个碗,下顿饭只需要2个碗,那么女友就可以只洗2个碗,这就属于“延时洗碗”,除非是用到了,否则就不洗~~因此:高效率(计算机中,不存在发霉的问题~~)
再比如说,有一个很大的文件(千万字的小说),咱们用编译器打开,那么就有两种加载方式:
1.把所有的内容,一次性全加载到内存中,再显示
2.只加载一部分并显示,后续如果用户翻页,随着翻页,再加载后续数据
那么第一种就会明显卡顿,而且就算加载这么多,你也未必看的过来,因此懒=高效率~
代码与饿汉模式基本类似👇
package thread; //通过懒汉模式构造单例模式 class Singletonlazy { private static Singletonlazy instance=null; public static Singletonlazy getInstance(){ //懒汉模式创建实例的时机,是在第一次使用的时候,而不是程序启动的时候 if(instance==null){ instance=new Singletonlazy(); } return instance; } private Singletonlazy(){ } } public class Demo28 { public static void main(String[] args) { Singletonlazy s1=Singletonlazy.getInstance(); Singletonlazy s2=Singletonlazy.getInstance(); System.out.println(s1==s2); } }
后续再创建实例一样会报错👇
单例模式,还有一些其他的写法,这里就不再一一展开了,日常工作开发中,最常用的就是以上两种方式,掌握好足够用了~~
闲聊:
其实上述懒汉模式/饿汉模式,还是存在缺陷的,比如可以通过反射的方式,来创建该类的实例,但反射本身就属于“非常规”的手段,日常开发,也不推荐使用反射,因为其副作用还是很明显的~
饿汉模式/懒汉模式的线程安全问题
咱们当前讲的这个章节是:多线程
上面谈到的单例模式,和线程有啥关系??
其实上述这些内容,都是引子而已,接下来才是正题~~
刚才编写的两份代码(饿汉/懒汉),是否是线程安全的?如果不是,该咋办??[非常经典的面试题]
本质就是在问:这两个版本的getInstance在多线程环境下调用,是否会出Bug~~
答疑:咱之前讲的时候不是说=这个赋值操作是原子的呀,+=、-=这些才是非原子的呀
单个=赋值操作,当然是原子的,但此时我们不能光看赋值操作,前面还有一个判定操作呢,这俩操作合在一起就是非原子的
下面拿时间线举个例子演示为啥懒汉模式线程不安全👇
这就属于覆盖了,而且随着第二个线程的覆盖操作,第一个线程new出来的对象,也会被GC给释放掉~~
不要忘记,new的这个对象,new的过程中,可能要把100G的数据从硬盘加载到内存呀~~
本来程序启动时间是10分钟,由于上述的Bug,加载两份,导致最终的时间要远远超过10分钟
由于存在这个Bug,懒汉模式这个写法,getInstance是线程不安全的~~
解决懒汉模式的线程不安全问题
加锁是一个常规手段~~
要怎么加锁呢?如下这种方式加吗?
public static Singletonlazy getInstance(){ if(instance==null){ synchronized (locker){ instance=new Singletonlazy(); } } return instance; }不是写了synchronized,代码就一定线程安全,一定得具体情况具体分析
此处是“条件修改”,我们希望,把 条件 和 修改 能够打包成原子的操作
因此正确的写法是这样的👇
public static Singletonlazy getInstance(){ synchronized (locker){ if(instance==null){ instance=new Singletonlazy(); } } return instance; }下面再拿时间线来演示一下👇
我们发现,引入加锁之后,后执行的线程就会在加锁的位置阻塞,阻塞到前一个线程解锁,当后一个线程进入条件的时候,前一个线程已经修改完毕,Instance不再为null,就不会进行后续的new操作
答疑:给方法加锁可以吗?
当然可以👇
public synchronized static Singletonlazy getInstance(){ if(instance==null){ instance=new Singletonlazy(); } return instance; }这个写法,相当于锁对象换成了类对象SingletonLazy.class
和之前的locker相比,没啥区别~~
加锁引入的新问题
答疑:死锁??
??一个线程一把锁,连续加锁两次(可重入,不构成死锁)
两个线程两把锁(但咱们这里只有一把锁)
一把锁,无法构成请求保持~~因此不构成死锁~~
我们仔细观察之前的代码👇
public static Singletonlazy getInstance(){ synchronized (locker){ if(instance==null){ instance=new Singletonlazy(); } } return instance; }我们发现,当把实例创建好了之后,后续再调用getInstance,都是直接执行return
如果只是进行 if 判定+return,这就纯粹属于读操作了
读操作,不涉及到线程安全问题
但是,每次调用上述的方法,都会触发一次加锁操作
虽然不涉及线程安全问题了,但多线程的情况下,这里的加锁,就会相互阻塞(必然存在锁竞争),从而影响程序的执行效率
这就属于“温饱思淫欲”:温饱=程序能够正确执行,没Bug,淫欲=已经没有Bug,希望能跑的更快,效率更高
怎么解决上述锁竞争引发的延时问题呢?
按需加锁,真正涉及到线程安全的时候,再加锁,不涉及的时候,就不加锁
简单说:
如果实例已经创建过了,就不涉及线程安全问题
如果实例还没创建呢,就涉及线程安全问题
其实只需要在外层多加一个判断就行了👇
public static Singletonlazy getInstance(){ if(instance==null){//判断是否需要加锁 synchronized (locker){ if(instance==null){//判断是否需要new对象 instance=new Singletonlazy(); } } } return instance; }其中:
synchronized (locker){ }这个代码并不止代表着加锁,也代表着可能会引起阻塞
一旦阻塞,对于计算机来说,阻塞的时间间隔,就是“沧海桑田”~~
有同学可能说上述代码连着写俩 if 看着别扭?
那是因为以往我们写的都是“单线程”的程序,单线程中,连续两个相同的 if ,是无意义的
单线程中,执行流就只有一个,上一个 if 的判定结果,和下一个 if 是一样的~
但是多线程中,两次判定之间,可能存在其他线程,就把 if 中的Instance变量给修改了,也就导致这里的两次 if 的结论可能不同~~
下面有请滑稽哥来演示一下上述这个过程👇
1.有三个线程,开始执行 getInstance ,通过外层的 if (instance == null) 知道了实例还没有创建的消息,于是开始竞争同一把锁
2.其中线程1率先获取到锁,此时线程1通知里层的 if (instance == null) 进一步确认实例是否已经创建,如果还没创建,就把这个实例创建出来
3.当线程1释放锁之后,线程2和线程3也拿到锁,也通过里层的 if (instance == null) 来确认实例是否已经创建,发现实例已经创建出来了,就不再创建了
4.后续的线程,不必加锁,直接就通过外层 if (instance == null) 就知道实例已经创建了,从而不再尝试获取锁了,降低了开销
其实上述代码还存在问题~
1.内存可见性问题
我们仔细观察上述代码👇
public static Singletonlazy getInstance(){ if(instance==null){ synchronized (locker){ if(instance==null){ instance=new Singletonlazy(); } } } return instance; }会不会出现 t1这个线程在读取 Instance 的时候,t2 线程进行修改呢?也就是说,是否存在“内存可见性”问题呢?
可能存在,不能说一定,因为编译器优化这个事情,是非常复杂的
但是为了稳妥起见,可以给 Instance 直接加上一个 volatile,从根本上杜绝内存可见性问题👇
private static volatile Singletonlazy instance=null;2.指令重排序问题
(之前说的指令重排序的线程问题在这里讲解~~~)
其实相比于内存可见性问题,这里更关键的是指令重排序
指令重排序:也是编译器优化的一种体现形式,编译器会在逻辑不变的前提下,调整你代码执行的先后顺序,以达到提升性能的效果
闲聊:编译器优化往往不只是javac自己的工作,通常是javac和JVM配合的效果(甚至是操作系统也要配合)
举个例子:妹子让你去买菜~~
告诉你买西红柿、鸡蛋、茄子、黄瓜,如果你按照这个顺序来买,可能路线是这样的👇
但如果你调整一下顺序:茄子、鸡蛋、黄瓜、西红柿,那么你的路线就是这样的👇
指令重排序,大前提是逻辑不变,但是在多线程环境下,这里的判定可能出现错误
上述代码中,指令重排序出现在这个位置👇
这行代码干了三件事儿:
1.申请内存空间
2.在空间上构造对象(初始化)
3.内存空间的首地址,赋值给引用变量
闲聊:
Java中虽然没有谈到“指针”这两个字
但是,可以把 引用 就视为一种“简化版”的指针
可以粗略的认为,引用里面保存的就是一个对象的内存地址
C++的引用和Java的引用区别还是很大的:
Java的引用,相比C++的引用/指针 来说,功能上做出了诸多的限制
之所以指针难、指针是精髓,主要是指针提供的功能太多了、用法太多了=>太灵活了(贬义词)
而安Java中的引用,就简单了很多~~
正常来说,这三个步骤,按照1→2→3这样的顺序来执行的
但是在指令重排序下,可能成为1→3→2这样的顺序
单线程环境下,1→2→3还是1→3→2其实无所谓~~
将1比作买房,2比作装修,3比作拿到钥匙,来举例子:
精装房(人家给你装修好了):1.买房→2.装修→3.拿钥匙
毛坯房(自己装修):1.买房→3.拿钥匙→2.装修
如果是1→3→2这样的顺序执行,多线程环境下,可能会出现Bug!!👇
可以发现,其他线程的代码在执行过程中,由于指令重排序,很可能拿着一个“未初始化”的对象进行操作,进而引发Bug~~
如何解决??
还是volatile👇
private static volatile Singletonlazy instance=null;volatile的功能有两方面
1.确保每次读取操作,都是读内存
2.关于该变量的读取和修改操作,不会触发重排序(这个更关键些)
本节课小结
到这里,我们的单例模式才算是大功告成~~
涉及到线程安全问题的时候,就涉及到3个要点:
①在正确的位置加锁
②双重 if ,确保执行效率
③加上volatile,避免内存可见性和指令重排序问题
面试中,真的遇到了原题,如何应对??(概率不小的情况)
1.先写一个不带线程安全的单例模式
2.思索片刻,线程不安全,把锁加上
3.再次思索片刻,加上 if
4.再次思索片刻,加上volatile
因为一次写出最终版本,在面试官眼里,他觉得这个问题,你正好准备过,此时说明这个问题就考察不出来啥,这题不算,谈下一个话题~~
我们需要把问题引导到你自己擅长的角度,把控整个面试的节奏~~
如果按上述3条依次写,面试官问一个你写一个,那么面试官就会觉得,你这边很可能没有准备过/很久之前看的,即使如此,你依然能够通过已经掌握的知识,推理出一些结论~~
答疑:
Q1:面试官要是觉得你菜,不给机会呢?
面试的时候,大部分面试官,看到你的回答有问题的时候,都会进一步去问的~
Q2:我是个诚实的孩子,骗人的事儿我做不来~
咱这不是“骗人”,咱这是“技巧”
找工作本身,应聘者就是弱势群体,公司和面试官的套路是层出不穷的,咱们有点小技巧,不是什么违背良心的事情~~
面试中有很多问题都是 送命题~
比如曾经在掌阅,面试官问:假如你手里有腾讯和字节的offer,你选哪个??
我选掌阅!!!你要是选别的,面试官会觉得,那跟我掌阅有啥关系,你去找别的去得了~
这就好比女朋友问你,杨幂好看还是迪丽热巴好看??
必然是你最好看~~
更多推荐

















所有评论(0)