目录
一、上节课复习回顾
在正式进入本篇博客之前,我们先来回顾一下上节课关于多线程的核心知识点:
-
线程的概念、进程和线程的区别
-
Thread类的使用
-
线程的状态
-
线程安全
-
锁 => 死锁
-
volatile
-
wait / notify、synchronized
二、本节课重点引入
本节课我们将深入探讨设计模式(单独的学科)。
设计模式背景
-
程序员水平参差不齐,大佬(少)、菜鸡(多)。
-
编译器优化只能解决执行效率的问题。
-
开发效率:程序员在写这个代码的时候,快慢。
-
目前来看,开发效率比执行效率更重要。
-
执行效率是可以通过升级硬件来弥补的(硬件性能突飞猛进,成本逐年降低)。
-
程序员的人力成本,逐年上升。
-
主要的成本,在人力成本上。
设计模式的意义
-
软件开发中,存在很多这样的重复的问题场景。
-
总结提炼出固定的套路/解决方案------定式。
-
设计模式类似于棋谱------按照棋谱来下,棋力一定不会弱。
-
大佬们总结出了很多的设计模式,不同的编程语言中也会存在不同的设计模式。
-
设计模式不是只有23种!!!
三、单例模式概念详解
一种设计模式,可以保证你一个Java进程中某个类只有唯一的一个实例。
为啥不能创建多个呢?
假设我们的服务器是要加载100GB的数据到内存中------广告系统。
一组服务器包含了若干个分片,每个分片中又有若干个服务器。
代码中搞了一个类DataCenter,类的构造方法中,进行上述的数据加载(读硬盘文件,把内容放到内存中)。
这个对象一旦创建好,就是消耗100GB的内存。
这个类,不能随便创建多个实例的。
不小心创建两个,都可能会导致程序出现严重问题。
某个类,本身从需求功能上来说,一个实例就足够了------完全可以使用单例模式了。
既然是要"单例",只new一次就好,不去new多次,有必要搞单例模式吗?
靠人来保证是不靠谱。
机器/程序/编译器则比较靠谱,
此处的单例模式,强制让某个类只能存在一个实例。
当你尝试创建多个实例,直接编译报错!
四、饿汉模式实现
只能确保当前进程内,没法保证多个进程之间~~
java
package thread;
// 此处要求 Singleton 类只能有一个实例。
class Singleton {
// 加了 static,当前的成员成为"类属性",在类对象上面的,类对象只有一个实例。
// 此处 instance 就可以保证在当前 java 进程中只有一份。
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
// 单例模式最关键的要点,禁止构造方法被外部使用。
private Singleton() {
}
}
public class Demo24 {
public static void main(String[] args) {
// 此时其他代码想 new 这个实例,直接编译报错。
// Singleton s = new Singleton();
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
五、饿汉模式原理剖析
每个 java => class => 被 JVM 加载到内存,得到类对象。
"饿" => 急迫------近似认为是程序一启动。
六、懒汉模式初探
java
package thread;
// 懒汉的单例模式
class SingletonLazy {
private static SingletonLazy instance = null;
// 懒汉模式的关键在于,把实例的创建时机推迟了,推迟到第一次使用的时候,创建。
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
七、两种设计模式对比
通常认为懒汉方式更好~~
八、懒汉模式线程安全问题探讨
懒是"褒义词"------懒,想偷懒,才思考,才去找新的方法,创造新的工具。
灵活 => 贬义词 => 容易出错 => 搞不好把年终奖给毙了~~
呆板 => 褒义词 => 不容易出错
设计模式,就是限制你的灵活~~
那么提一个问题:这俩版本是否是线程安全的??
多线程------多个线程并发的调用 getInstance,是否会有问题~~
先明白,return 是一个"读操作"。
++所以饿汉模式,天然就是线程安全的。++
getInstance中只是进行读操作。
多个线程对同一个变量进行读取,没有问题的~~
++那么,懒汉模式呢?先说结论,他是线程不安全的!++
因为条件判定和赋值,放到一起,才算是完整的逻辑~~才是原子性。
但是懒汉模式虽然线程不安全,不安全只是出现在实例化之前的时候。
一旦实例创建好了,后续再调用 getInstance,都不会有问题了(都是读操作了)。
九、懒汉模式加锁尝试
此处本质上是 if 和 = 这两个操作被打包成原子。
十、双重检查锁定实现
进一步分析------
此处的这个代码,只有在实例化之前存在线程安全问题。
一旦实例化完毕之后,线程安全问题就消失了~~
此处即使把 instance 实例化之后,此处的代码的仍然会加锁 ~~
正常如果不加锁的话,代码就不会产生阻塞~~
虽然加锁内部的执行逻辑看似很块,实际上它背后线程调度的成本是非常高的~~
对于单线程来说,连续写两次的条件,结论一定是相同的!!不需要重复写~~
对于多线程来说,就不适用了~~
走到那 => 阻塞 => 很长时间。
锁阻塞的时间里,很可能存在其他线程,把 instance 给修改了。
导致外层 if 和 里层 if 结论不相同了!!
完整合适的代码:
java
package thread;
// 懒汉的单例模式
class SingletonLazy {
private volatile static SingletonLazy instance = null;
// 作为加锁锁的对象,由于是要在 static 方法中使用该对象,对象本身也得是 static 的。
private static Object locker = new Object();
// 懒汉模式的关键在于,把实例的创建时机推迟了,推迟到第一次使用的时候,创建。
public static SingletonLazy getInstance() {
//第一个if语句用来判断是否还需要加锁
if (instance == null) {
synchronized (locker) {
// 第二个if语句判定是否需要创建实例
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
🌟这也是一个非常经典的秋招面试题:
你需要回答清楚三个点:
为什么要有两个if,他们分别的作用各是什么
为什么要加锁
为什么instance这个引用变量要加volatile关键字?(主要解决指令重排序问题)
加锁的目的分析:
十一、指令重排序问题与volatile修饰
指令重排序引起的问题~~也是编译器的一种优化手段~~
本质是调整你指令执行的顺序~~从而让代码的效率更高~~
此处应该禁止指令重排序,把这样的编译器优化关闭掉~~
volatile------
++内存可见性 / 指令重排序优化都是出现在针对某个变量的 读写 操作中~~++
当使用 volatile 修饰 instance 的时候,此时围绕 instance 变量读写相关的优化,就都被关闭了~~
确保从内存进行读写,确保操作顺序不会被打乱~~
十二、总结
上述逻辑在多线程下可能就出问题了!!!
相关的例子,只有单例模式这一个孤证~~
针对指令重排序对线程安全的影响,只是根据网上的常见说法汇总的情况,真实性不可考,
没法写代码验证~~ 不可控因素太多了~~
我个人角度是存疑的,
很可能在现在 jdk 的版本中,即便不加 volatile 也没事了~~
不过宁可信其有,不可信其无。
写代码的时候多个 volatile 倒是没啥成本~~
面试官也是这么理解的~~
