单例模式详解(饿汉式,懒汉式,登记式,枚举式,序列化和反序列)

一、单例模式应用场景:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

应用场景:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 Spring 中的单例模式完成了后半句话,即提供了全局的访问点 BeanFactory。但没有从构造器级别去 控制单例,这是因为 Spring 管理的是是任意的 Java 对象。 Spring 下默认的 Bean 均为单例。

为了解决并发访问的时候线程安全问题,保证单例的技术方案有很多种,饿汉式,懒汉式,注册登记式,枚举式,序列化和反序列化的时候出现多例。

①饿汉式:在实例使用之前,不管你用不用,我都先new出来再说,避免了线程安全问题;

 

//饿汉式单例
// 它是在类加载的时候就立即初始化,并且创建单例对象
    //优点:没有加任何的锁、执行效率比较高,
    //在用户体验上来说,比懒汉式更好

    //缺点:类加载的时候就初始化,不管你用还是不用,我都占着空间
    //浪费了内存,有可能占着茅坑不拉屎

    //绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题
public class Hungry {

    private Hungry(){}
    //先静态、后动态
    //先属性、后方法
    //先上后下
    private static final Hungry hungry = new Hungry();

    public static Hungry getInstance(){
//        Hungry hungry;

        return  hungry;
    }

}

②懒汉式:默认加载的时候不实例化,在需要用这个实例的时候才实例化,延时加载

五种情况:不加锁的情况,方法上加锁,匿名内部类的形式,变量上加锁,volatile终极式版本

第一种实现实例:

 

//懒汉式单例

    //在外部需要使用的时候才进行实例化
public class LazyOne {
    private LazyOne(){}


    //静态块,公共内存区域
    private static LazyOne lazy = null;

    public static LazyOne getInstance(){

        //调用方法之前,先判断
        //如果没有初始化,将其进行初始化,并且赋值
        //将该实例缓存好
        if(lazy == null){
            //两个线程都会进入这个if里面
            lazy = new LazyOne();
        }
        //如果已经初始化,直接返回之前已经保存好的结果

        return lazy;

    }

}

第二种实现实例方法:

 

/**
 * Created by Tom on 2018/3/7.
 */
public class LazyTwo {

    private LazyTwo(){}

    private static LazyTwo lazy = null;

    public static synchronized LazyTwo getInstance(){

        if(lazy == null){
            lazy = new LazyTwo();
        }
        return lazy;

    }

}
第三种方法:
//懒汉式单例
    //特点:在外部类被调用的时候内部类才会被加载
    //内部类一定是要在方法调用之前初始化
    //巧妙地避免了线程安全问题

    //这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
    //完美地屏蔽了这两个缺点
    //史上最牛B的单例模式的实现方式
public class LazyThree {

    private boolean initialized = false;

    //默认使用LazyThree的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的

    private LazyThree(){

        synchronized (LazyThree.class){
            if(initialized == false){
                initialized = !initialized;
            }else{
                throw new RuntimeException("单例已被侵犯");
            }
        }

    }


    //每一个关键字都不是多余的
    //static 是为了使单例的空间共享
    //保证这个方法不会被重写,重载
    public static final LazyThree getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }


    //默认不加载
    private static class LazyHolder{
        private static final LazyThree LAZY = new LazyThree();
    }


}
第四种方法
 

 public class LazyFour {

    private LazyFour(){}
    private final static LazyFour lazyFour = null;
    private final static LazyFour getInstance(){
        if (lazyFour == null) {
            synchronized (LazyFour.class){
                if (lazyFour == null) {
                    return  new LazyFour();
                }
               
            }
        }

        return lazyFour;
    }
}
第五种
使用volatile 的懒汉式单例模式
1. 给 singleton 分配内存

2. 调用 Singleton 的构造函数来初始化成员变量,形成实例

3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。

这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作。

 

volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),
这样,在它的赋值完成之前,就不用会调用读操作。/**
 * 终极版本
 */
public class LazyFive {
    private static volatile LazyFive instance;
    private LazyFive() {}
    public static LazyFive getInstance() {
        if (instance == null) {
            synchronized (LazyFive.class) {
                if (instance == null) {
                    instance = new LazyFive();
                }
            }
        }
    return instance;
    }
}

枚举式单例模式:可以参考这篇博客https://www.cnblogs.com/cielosun/p/6596475.html(转载)

 

//常量中去使用,常量不就是用来大家都能够共用吗?
    //通常在通用API中使用
public enum Color {
    RED(){
       private int r = 255;
       private int g = 0;
       private int b = 0;

    },BLACK(){
        private int r = 0;
        private int g = 0;
        private int b = 0;
    },WHITE(){
        private int r = 255;
        private int g = 255;
        private int b = 255;
    };
}

③注册登记式:每使用一次,都往一个固定的容器中去注册并且将使用过的对象进行缓存,下次去取对象的时候,就直接从缓存中取值,以保证每次获取的对象都是同一个对象,IOC的单例模式,就是典型的注册登记是单例

登记式实际对一组单例模式进行的维护,主要是在数量上的扩展,通过map我们把单例存进去,这样在调用时,先判断该单例是否已经创建,是的话直接返回,不是的话创建一个登记到map中,再返回。对于数量又分为固定数量和不固定数量的。下面采用的是不固定数量的方式,在getInstance方法中加上参数(string name)。然后通过子类继承,重写这个方法将name传进去。让我们看看代码吧。

 

//Spring中的做法,就是用这种注册式单例
public class BeanFactory {

    private BeanFactory(){}

    //线程安全
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();

    public static Object getBean(String className){

        if(!ioc.containsKey(className)){
            Object obj = null;
            try {
                obj = Class.forName(className).newInstance();
                ioc.put(className,obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return obj;
        }else{
            return ioc.get(className);
        }

    }

}

④序列化和反序列划保证单例:重写readResolve; 

 

//反序列化时导致单例破坏
public class Seriable implements Serializable {


    //序列化就是说把内存中的状态通过转换成字节码的形式
    //从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
    //内存中状态给永久保存下来了

    //反序列化
    //讲已经持久化的字节码内容,转换为IO流
    //通过IO流的读取,进而将读取的内容转换为Java对象
    //在转换过程中会重新创建对象new


    public  final static Seriable INSTANCE = new Seriable();
    private Seriable(){}

    public static  Seriable getInstance(){
        return INSTANCE;
    }

    private  Object readResolve(){
        return  INSTANCE;
    }

}

测试用例

 

public class SeriableTest {
    public static void main(String[] args) {

        Seriable s1 = null;
        Seriable s2 = Seriable.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("Seriable.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();


            FileInputStream fis = new FileInputStream("Seriable.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (Seriable)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值