什么是单例模式
单例模式是指在内存中只会创建且仅创建一次对象的设计模式
在程序多次使用同一个类的对象且作用相同时,为了防止频繁创建对象使内存飙升,单例模式可以在内存中创建这个类的一个对象,供所有需要用到的地方共享这个对象
特点
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
类型
- 懒汉式:需要使用这个对象的时候才会去创建这个单例对象
- 饿汉式:在类加载的时候就创建了这个单例对象
懒汉式单例
在程序使用这个对象的时候,去判断这个对象是否为空,若已存在,就直接返回这个对象,若不存在,就去执行实例化操作
在第一次调用getInstance方法的时候实例化
public class Singleton {
public static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance(){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
懒汉式单例通过使用private修饰构造方法保证Singleton在外部无法被实例化,只能通过调用静态方法getInstance来实例化Singleton的唯一实例
(其实也可以通过反射机制来实例化private构造方法的类,但通过反射都会使所有的单例模式失效,所以在此不做讨论)
以上懒汉式单例是线程不安全的,在并发下可能会出现多个Singleton实例,那么如果实现一个线程安全的懒汉式呢?我们会在后面讲解
饿汉式单例
在类加载的时候就创建了这个单例对象,程序需要的时候直接返回这个单例对象即可
在最开始就已经创建了一个实例对象在内存中,getInstance方法直接返回这个对象即可,当类被
public class Singleton {
private static final Singleton singleton = new Singleton();
public Singleton() {
}
public static Singleton getInstance(){
return singleton;
}
}
解决懒汉式线程安全问题
看一下懒汉式的getInstance方法
public static Singleton getInstance(){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
此时如果有两个线程,在调用getInstance方法时都同时判断了singleton为空,那么就会各自实例化一个singleton对象,这就不是单例了,线程不安全
既然会有多个线程同时判断的情况,那我们加上锁不就好了么,由此引出第一种方法
加锁
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
或者
public static Singleton getInstance() {
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
加锁之后在多线程的情况下就会竞争锁,就可以保证在同一时间下只有一个线程判断singleton是否为空了,但是就出现了另外一个问题:每次都会去竞争锁,在高并发下性能会很差
这样的话,在方法上加锁的方法就不能用了,因为无论如果都会发生锁竞争的情况,由此引出双重检查锁定这一方法
双重检查锁定
public class Singleton {
public static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance(){
if (singleton == null) {
synchronized (Singleton.class){
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这样就可以同时实现线程安全和性能高效了,分析如下:
- 第一次判空:
如果singleton不为空,直接返回单例对象,如果singleton为空,线程都进入分支,都不需要竞争锁
- 加锁:
锁竞争,只有一个线程能抢到锁
- 第二次判空:
因为singleton可能已经被之前的线程实例化了,所以还要判一次空,如果singleton不为空,直接返回单例对象,如果singleton为空,就会实例化对象
- 实例化之后:
之后进入该方法的线程都不会再发生锁竞争了,因为在第一次判空的时候就直接返回对象了
此时代码已经很完美了,但是还有一个问题:指令重排
使用volatile防止指令重排
在创建对象的时候,JVM会经历三步:
- 为singleton分配内存空间
- 初始化singleton对象
- 使singleton指向分配的内存空间
指令重排序:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能
如果发生指令重排序,可能创建对象时三步的顺序变为了1 3 2,在多线程的情况下,某个线程可能会先进行了1 3步,然后其他线程在判断singleton是否为空的时候,判断结果会是不为空,但是第2步还没有完成呢,导致其他线程得到的是一个还没有初始化的singleton对象
我们可以使用volatile关键字来防止指令重排序,原理可以自行搜索
使用volatile关键字还可以保证singleton对象的可见性,在同一时间在内存中都是最新的那个值,每次操作这个对象的时候都会获取这个对象的值
public class Singleton {
public static volatile Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance(){
if (singleton == null) {
synchronized (Singleton.class){
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
破坏懒汉式单例与饿汉式单例
利用反射和序列化可以破坏这两个单例模式
反射
强制访问私有构造器去创建对象
public static void main(String[] args) {
// 获取类的显式构造器
Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton obj1 = construct.newInstance();
// 通过正常方式获取单例对象
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
序列化与反序列化
readObject()方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
枚举实现单例模式
JDK1.5之后,也可以使用枚举来实现单例模式
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("这是枚举单例模式!");
}
}
优点
- 对比懒汉式和饿汉式,代码更加简洁
- 可以保证线程是安全的和对象是单一的
测试一下
public class Test {
public static void main(String[] args) {
Singleton singleton1 = Singleton.INSTANCE;
Singleton singleton2 = Singleton.INSTANCE;
System.out.println(singleton1 == singleton2);
}
}
- 防止反射和序列化来破坏单例模式
在使用反射来调用newInstance()方法时,会先判断是否为枚举类,如果是,会报异常
在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作
总结
- 两种单例模式:懒汉式和饿汉式
- 懒汉式:在程序使用这个对象的时候,去判断这个对象是否为空,若已存在,就直接返回这个对象,若不存在,就去执行实例化操作,使用双重检查锁定可以实现线程安全和性能高效
- 饿汉式:在类加载的时候就创建了这个单例对象,程序需要的时候直接返回这个单例对象即可
- 对内存要求高的话,使用懒汉式,只有需要的时候才实例化
- 对内存要求不高的话,使用饿汉式,比较简单,线程安全和性能也不会下降
- 指令重排序会导致获取空对象,可以通过volatile关键字来防止指令重排序
- 使用枚举来实现单例模式是最好的,代码简洁,线程安全,不会由于反射和序列化破坏单例模式