文章目录
1.单例模式
单例模式确保某个类在程序中只有一个实例,避免多次创建实例(禁止多次使用 new),要实现这一点的关键在于将类的所有构造方法声明为private,这样类外无法直接访问构造方法, new操作会编译时报错,从而保证类的实例的唯一性
只有一个实例,这样的要求,开发中是常见的需求场景,比如MySQL中的JDBC编程的第一步 DataSource(描述了数据库服务器在哪里,URL,user,password),这样的场景非常适合于作为单例,描述数据库的信息,类似于存储数据库信息这样的对象,由于数据库只有一份,即使搞多个这样的对象,也没啥意义,也是一样的信息
单例模式的实现方式主要有两种:饿汉方式 和 懒汉方式
1)饿汉模式
类加载的同时,创建实例
//通过饿汉模式构造单例模式
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);
// Singleton t3 = new Singleton();
}
}
2)懒汉模式
- 饿 是尽量早的创建实例
- 懒 是尽量晚的创建实例(甚至可能不创建了) 延迟创建
- 懒的另一个含义 高效率~~
- 如果说,饿汉模式是在类加载的时候(一个比较早的时期),进行创建实例,并且使用private修饰所有的构造方法,使得代码中无法创建该类的其他实例
- 那么懒汉方式的核心思路,就是延迟创建实例,真正用到实例,再去创建,甚至可能不创建实例,这样可以减小开销,提升效率
①.单线程版本
class SingletonLazy{
//懒汉模式-单线程
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
②.多线程版本
class SingletonLazy{
private static SingletonLazy instance = null;
private static Object locker = new Object();
public static SingletonLazy getInstance(){
if(instance == null){
synchronized (locker){
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);
}
}
2.分析单例模式里的线程安全问题
刚才编写的两份代码(饿汉和懒汉),是否是线程安全的?如果不安全如何解决问题?–这两个模式下的getInstance在多线程环境下调用,是否会出bug~
1)饿汉模式
2)懒汉模式
单线程版本的懒汉模式,如果在多线程环境下运行会出现什么问题?
- instance 被 static 修饰说明这个一块内存,却被多个线程调用getInstance(),同一块空间被修改多次,并且该赋值操作不是原子的
- getInstance()方法,不仅有读操作,还有写操作(满足if条件才赋值,不满足if条件不会赋值),所以判断条件和赋值这两个操作是密不可分的,就是因为无法保证这两步操作无法紧密执行,就会出现线程安全问题
懒汉模式是如何出现线程安全问题的
要了解这个问题,我们可以通过画时间轴来更直观的感受
- ①.判断条件确实为NULL,OK那么线程1继续向下执行,不会跳出if代码块
- ②.判断条件确实为NULL,OK那么线程2继续向下执行,不会跳出if代码块
- ③.线程1可以继续向下执行,执行到new了一个对象,然后return
- ④.线程2可以继续向下执行,执行到new了一个对象,然后return
- 这里就出现了一个很大的问题 ! !
随着线程2的创建实例,这个操作覆盖掉了,线程1new出来的对象,线程1new出的对象被GC给释放掉了~~
第一次new这个对象的时候,是会进行加载数据的,有可能我们的数据达到100G,100G的数据从硬盘加载到内存 大概要十分钟
本来只需要十分钟,可以由于上述BUG,加载了两份,导致我们的启动时间逼近20分钟
这就与我们的预期不符,妥妥的BUG
3.解决问题
那我们如何解决这个线程安全问题呢? 常规方法:加锁!
- 显然不行,不是加了synchronized就会线程安全的
- 我们要具体的分析具体代码
- 前面分析过了,是判断条件和赋值操作,这两个操作一起决定的代码在多线程环境下是非原子操作
- 也就是说,修改是原子的,但是此处为"条件修改"
- 我们希望,条件判断和修改打包成原子操作
- 引入synchronized锁之后,后执行的线程就会在加锁的位置阻塞,阻塞到前一个线程解锁
- 当后一个线程进入判断条件时,前一个线程已经修改完毕,instance不再为NULL,就不会执行后续的new操作
- 后续再调用getInstance,此时都是直接执行 return
- if + return 就是纯粹的读操作,读操作不涉及线程安全问题
进一步优化
加锁导致的执行效率优化
- 虽然不涉及线程安全问题了,但是每次调用上述getInstance方法,都会触发一次加锁操作
- 多线程情况下,这里的加锁,就是相互阻塞,影响程序的执行效率
- 一旦阻塞,此时对于计算机来说,阻塞的时间间隔,就是"沧海桑田",不知道啥时候才能调度
- 我们应该按需加锁,真正涉及到线程安全的时候,再加锁,不涉及,就不加
- 加锁时机:若实例已经创建过了,就不涉及线程安全问题,没创建,就涉及线程安全问题
- 以往都是"单线程"程序,单线程中.连续两个相同的 if ,是无意义的
- 单线程中,执行流是只有一个的 ,上一个if的判断结果和下一个if的是一样的
- 多线程中,两次判定之间,可能存在其他线程就把 if 中的 instance变量修改了 也导致这里的两次if的结论可能不同
预防内存可见性问题
- 是否会存在内存可见性问题?
- 可能会存在,编译器优化这个事情,非常复杂
- 编译器优化往往不只是 javac自己的工作,通常是javac和jvm配合的效果(甚至是操作系统也要配合)
- 为了稳妥起见,可以给instance直接加上一个volatile,从根本上杜绝,内存可见性问题
private static volatile SingletonLazy instance = null;
4.解决指令重排序问题
- 这里更关键的问题,是指令重排序导致的线程安全问题
- 指令重排序也是编译器优化的一种体现形式,编译会在逻辑不变的前提下,调整代码的执行顺序,来达到提升性能的效果
举个栗子~~
比如,我们去买菜,我们买西红柿,鸡蛋,茄子,黄瓜
第二幅图的执行顺序,明显效率高多了
比如,我们生活中会出现的指令重排序情况
- 1.买房 2. 装修 3.拿到钥匙
- 1.买房 3.拿到钥匙 2.装修
- volatile 的功能有两方面
- 1.确保每次读取操作,都是读内存 -->确保内存可见性
- 2.关于该变量的读取和修改操作,不会触发重排序 -->避免指令重排序问题