单例模式

  1. 云栖社区>
  2. 博客>
  3. 正文

单例模式

~信~仰~ 2018-11-20 12:05:34 浏览806
展开阅读全文

单例模式的定义如下:

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例类自身保存它的唯一实例,这个类保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。

单例模式的一些特点:

构造方法私有化,防止外部通过访问构造方法创建对象;

提供一个全局方法使其单例对象被外部访问;

考虑多线程并发情况的单例唯一性。

单例模式的几种实现方式:

懒汉式、饿汉式、内部类、注册式、枚举类

这里强烈推荐的是内部类和枚举类的实现方式。

饿汉式

在类加载的时候就立即创建单例对象。

  • 优点:绝对的线程安全,无锁,执行效率高;
  • 缺点:即使单例在程序中一直用不到,也会在类加载的时候初始化,不管用或不用,都占据内存空间。
package com.faith.net;

/**
 * 饿汉式
 */
public class Singleton {

    private Singleton(){}

    private static final Singleton hungry = new Singleton();

    public static Singleton getInstance(){
        return  hungry;
    }
}

懒汉式

当需要使用单例的时候才进行实例化。

package com.faith.net;

/**
 * 懒汉式
 */
public class Singleton {

    private Singleton(){}
    
    private static Singleton lazy = null;

    public static Singleton getInstance(){
        if(lazy == null){
            lazy = new Singleton();
        }
        return lazy;
    }
}

需要注意的是以上获取单例不是线程安全的。

  • 用synchronized改版
package com.faith.net;

/**
 * 懒汉式
 */
public class Singleton {

    private Singleton(){}

    private static Singleton lazy = null;

    public static synchronized Singleton getInstance(){
        if(lazy == null){
            lazy = new Singleton();
        }
        return lazy;
    }
}

通过synchronized关键字保证了线程安全,但这种排队方式的处理在多线情况下效率较低。

双重锁定

双重锁定是懒汉式的一种实现方式,上面例子中synchronized关键字加在了方法上,导致了每个线程都会排队获取对象。

双重锁定的方式是不让线程每次都加锁,而是只在实例未被创建的情况下加锁,同时也能保证线程安全,这种做法称为双重锁定(Double-Check Locking)。

示例如下:

package com.faith.net;

import java.io.Serializable;

/**
 * 单例类
 */
public class Singleton implements Serializable{
    
    private volatile static Singleton singleton;
    
    private Singleton (){}
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
    private Object readResolve() {
        return singleton;
    }
}

内部类

这种方式使用了内部类的一些特性:

  • 内部类只有在外部类被调用的时候才会被加载;
  • 内部类在方法调用之前完成初始化;
  • 其次,类的加载机制保证了每个类只会被加载一次。

这是非常推荐的一种方式,它综合了懒汉式延迟加载和饿汉式线程安全的特性。

package com.faith.net;

/**
 * 内部类方式
 */
public class Singleton {

    private boolean initialized = false;
    
    private Singleton(){}

    /*
     * static 保证单例共享,final保证方法不被重写
     */
    public static final Singleton getInstance(){
        return LazyHolder.LAZY; // 在返回结果以前,会先加载内部类
    }

    // 内部类
    private static class Singleton{
        private static final Singleton LAZY = new Singleton();
    }
}

注册式

注册式是spring IOC容器使用的一种方式,通过将实例保存到Map容器中实现。

package com.faith.net;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 注册式
 */
public class BeanFactory {

    private BeanFactory(){}
    
    private static Map<String,Object> container = new ConcurrentHashMap<String,Object>();

    public static synchronized Object getBean(String className){
        if(!ioc.containsKey(className)){
            Object obj = Class.forName(className).newInstance();
            container.put(className,obj);
            return obj;
        }else{
            return container.get(className);
        }
    }
}

枚举类

枚举有如下特性:

  • 枚举对象,例如下面的INSTANCE由枚举机制保证了一定会是单例的;
  • 枚举的加载机制保证了线程安全;
  • 枚举保证了实例不会被反射破坏;
  • 枚举的对象只有被使用时才会进行实例化,保证了延迟加载。

所以通过枚举实现,也会保证代码的高效、线程安全以及延迟加载的特性。

实现方式
  • 返回自身对象
package com.faith.net;

public enum Singleton {

    INSTANCE; // 单例对象

    // 这里可以添加多个成员方法,例如
    public void doSomething() {
        // 省略具体代码
    }
}
  • 返回其他对象,例如ConcurrentHashMap
package com.faith.net;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public enum Singleton {

    INSTANCE; // 枚举对象,由于枚举机制,这个也是单例的

    private Map instance; // 需要做成单例的对象

    Singleton() {
        instance = new ConcurrentHashMap();
    }

    public Map getInstance() {
        return instance;
    }
}

反序列化对单例的破坏

对象的序列化指将对象转化为字节流;反序列化指将字节流转化为相应的对象。

反序列化的特点是,默认情况下,会根据字节流创建一个新的对象。那么这种特性会导致破坏单例。如下:

  • 创建单例类
package com.faith.net;

import java.io.Serializable;

/**
 * 单例类
 */
public class Singleton implements Serializable{
    
    private volatile static Singleton singleton;
    
    private Singleton (){}
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
  • 客户端测试反序列化
package com.faith.net;

import java.io.*;

public class SerializableDemo1 {

    public static void main(String[] args) throws Exception {
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(Singleton.getSingleton());
        
        // 反序列化
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        Singleton newInstance = (Singleton) ois.readObject();
        
        // 判断
        System.out.println(newInstance == Singleton.getSingleton());
    }
}

输出会为false。这种情况可以通过在单例类中添加readResolve方法解决,如下:

package com.faith.net;

import java.io.Serializable;

/**
 * 单例类
 */
public class Singleton implements Serializable{
    
    private volatile static Singleton singleton;
    
    private Singleton (){}
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
    private Object readResolve() {
        return singleton;
    }
}

因为java对象的反序列化通过ObjectInputputStream实现的,通过readObject方法读取字节流并创建新的对象。

但在readObject方法中有一个判断,内容是判断被反序列化的类中是否包含readResolve方法,如果包含readResolve方法,就直接返回readResolve方法中的逻辑,因此readResolve能避免反序列化对单例模式的破坏。

反射对单例的破坏

懒汉式、饿汉式都可以被反射破坏,如下:

    public static void main(String[] args) {
        Class<?> clazz = Singleton.class;

        //通过反射拿到私有的构造方法
        Constructor c = clazz.getDeclaredConstructor(null);
        //强制访问私有构造
        c.setAccessible(true);
        
        //调用了两次构造方法,相当于new了两次
        Object o1 = c.newInstance();
        Object o2 = c.newInstance();
        
        System.out.println(o1 == o2);
}

而枚举可以避免被反射破坏,因为枚举不能被反射方式访问。

网友评论

登录后评论
0/500
评论
~信~仰~
+ 关注