目录
一、前言
单例设计模式:一个类永远只存在一个对象,不能创建多个对象。
为什么要用单例?
开发中有很多类的对象我们只需要一个,例如虚拟机对象、任务管理器对象;对象越多越占用内存,有些时候只需要一个对象就可以实现业务,单例可以节约内存,提高性能
二、饿汉单例模式
在用类获取对象的时候,对象已经提前创建好,可以直接拿来用
因为对象提前创建好了,所以不存在线程安全的问题
设计步骤:
- 私有构造器
- 定义一个静态变量存储一个对象
- 创建一个返回单例对象的方法
public class Singleton {
//在用类获取对象的时候,对象已经提前创建好了
// 1.私有构造器
private Singleton() { }
// 2.定义一个静态变量存储对象
private static final Singleton singleton = new Singleton();
// 3.创建一个返回单例对象的方法
public static Singleton getInstance() {
return singleton;
}
}
class Singletontest{
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1==instance2);//true
}
}
饿汉弊端:如果没有直接使用的话,因为一次性都加载进来了,如果对象过大,会浪费大量的内存空间,影响性能
三、懒汉单例模式
在需要该对象的时候,类才去创建一个对象(延迟加载对象),存在线程安全问题!
设计步骤:
- 私有构造器
- 定义一个静态变量存储一个对象
- 创建一个返回单例对象的方法(第一次来拿对象时需要创建一个对象)
问题:多线程下会创建多个对象
public class Singleton {
//需要用到对象时才去创建
// 1.私有构造器
private Singleton() { }
// 2.定义一个静态变量存储对象
private static Singleton singleton;
// 3.创建一个返回单例对象的方法
public static Singleton getInstance() {
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
class Singletontest{
public static void main(String[] args) {
new SingletonThread().start();
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println("mainThread:"+instance1.hashCode());
System.out.println("main线程的对象:"+ (instance1==instance2));//true
}
}
class SingletonThread extends Thread{
@Override
public void run() {
Singleton instance = Singleton.getInstance();
System.out.println("SingletonThread:"+instance.hashCode());
}
}
运行截图:(创建了两个对象)
解决A:直接加synchronized锁
优化getInstance方法,加锁,但是降低了效率
public static synchronized Singleton getInstance() {
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
运行结果:(hashcode值一样了,是同一个对象)
解决B:synchronized锁+DCL(Double Check Lock)
优化getInstance方法,具体如下
public static Singleton getInstance() {
if(singleton==null){//第一次检查
synchronized (Singleton.class) {//synchronized把整个Singleton类锁住
if(singleton==null)//第二次检查
singleton = new Singleton();
}
}
return singleton;
}
Q:为什么要第二次检查?
A:设想一下,在最开始,如果N个线程同时并发来获取实例,除了获取到锁的线程之外其他的线程都阻塞在第2步,等待第一个线程初始化实例完成后。而后面的N - 1线程会串行执行synchronized代码块,如果代码块中没有判断singleton是否为null,则还会再new出 N - 1 个实例出来
因此这里的DCL机制是必须的。
指令重排序问题(解决:volatile)
挖坑jvm
什么是指令重排序?
指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。
Java中新建一个对象分为三个步骤:
- 在内存中开辟一块地址
- 对象初始化
- 将指针指向这块内存地址
因此Java创建对象不是原子操作,而synchronized 块里的非原子操作依旧可能发生指令重排
《Java 并发编程实战》提到:
有 synchronized 无 volatile 的 DCL(双重检查锁) 会出现的情况:线程可能看到引用的当前值,但对象的状态值确少失效的,这意味着线程可以看到对象处于无效或错误的状态。
具体如图:
线程1 | 线程2 | |
---|---|---|
1 | 分配内存 | |
2 | 将指针指向这块内存地址 | |
3 | 判断对象是否为null | |
4 | 由于对象不为null,返回对象 | |
5 | 初始化对象 |
如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
而volatile除了保证数据的可见性外,还可以禁止指令的重排序。详见JSR133的Java内存模型和线程规范
解决:在定义静态变量时添加volatile关键字
具体代码
private volatile Singleton singleton;
解决C:静态内部类
静态内部类解决办法采用类的装载机制保证初始化实例时只有一个线程
类的静态属性(类)只会在第一次加载类的时候初始化
JVM会帮助我们在类初始化时保证线程的安全性,别的线程无法进入
详见《深入理解java虚拟机》
具体代码:
public class Singleton {
private Singleton() { }
private static Singleton singleton;
//不会在外部类初始化时就直接加载
//只有当调用了getInstance方法时才会静态加载
private static class SingletonInnerClass{
//线程安全:final保证了在内存中只有一份
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonInnerClass.instance;
}
}
上述三种解决方案看似完美,但Java存在反射机制呀
四、反射破坏单例问题
反射只需要setAccessible就能破坏构造器的私有化呀
解决:Enum枚举
枚举类似类,一个枚举可以拥有成员变量,成员方法,构造方法
当反射遇到枚举时直接抛出异常
具体代码:
public enum Singleton {
instance;
public static Singleton getInstance() {
return Singleton.instance;
}
}
class Singletontest{
public static void main(String[] args) {
new SingletonThread().start();
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println("mainThread:"+instance1.hashCode());
System.out.println("main线程的对象:"+ (instance1==instance2));//true
}
}
变得更强,这里有篇把单例写的特别好mark https://blog.csdn.net/weixin_36586120/article/details/105522491