设计模式之单例模式(创建型,保证独一无二)

介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。

饿汉式单例

场景:类的构造函数定义为private的,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,这时候线程还不存在,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
代码:

public class Hungry {
    private static Hungry hungry=new Hungry();
    private Hungry() {
    }
    private static Hungry getInstance(){
        return hungry;
    }
}

懒汉式单例

场景1:单例在需要的时候才去创建,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。
代码1:

public class LazyOne {
    private static LazyOne lazyOne=null;
    private LazyOne() {
    }
    private static LazyOne getInstance(){
        if(lazyOne==null){
            lazyOne = new LazyOne();
        }
        return lazyOne;
    }
}

场景2:场景1的懒汉模式并没有考虑线程安全问题,因为多个线程可能会并发调用getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题,实现如下。
代码2:

public class LazyOne {
    private static LazyOne lazyOne=null;
    private LazyOne() {
    }
    private static synchronized LazyOne getInstance(){
        if(lazyOne==null){
            lazyOne = new LazyOne();
        }
        return lazyOne;
    }
}

场景3:场景2加锁的懒汉模式看起来解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此就有了双重校验锁,可以看到下面在同步代码块外多了一层instance为空的判断,由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。因此,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。不过还需要考虑一种情况,假如两个线程A、B,A执行了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (instance == null)语句,确保只能有一个线程创建单例。
代码3:

public class LazyOne{
    private static LazyOne lazyOne=null;
    private LazyOne() {
    }
    private static LazyOne getInstance(){
        if(lazy==null){
            synchronized (lazyOne.class){
                if(lazyOne==null){
                    lazyOne= new LazyOne();
                }
            }
        }
        return lazyOne;
    }
}

场景4:我们看到双重校验锁既实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。而new LazyOne()不具有原子性,可能会出现指令重排的情况,因此需要加上volatile关键字,用来禁止指令重排。
代码4:

public class LazyOne{
    private static volatile LazyOne lazyOne=null;
    private LazyOne() {
    }
    private static LazyOne getInstance(){
        if(lazy==null){
            synchronized (lazyOne.class){
                if(lazyOne==null){
                    lazyOne= new LazyOne();
                }
            }
        }
        return lazyOne;
    }
}

静态内部类方式:
场景:以下方式同样利用了类加载机制来保证只创建一个instance实例,它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
代码:

public class LazyThree {
    private static boolean initialized=false;
    private LazyThree() {//用来测试外界是否多次创建单例,主要是考虑反射调用
        synchronized (LazyThree.class){
            if(initialized==false){
                initialized=!initialized;
            }else {
                throw new RuntimeException("单例已被侵犯!");
            }
        }
    }
    //static 是为了使单例的空间共享
    //final 保证这个方法不会被重写、重载
    private static final LazyThree getInstance(){
        //在返回结果以前,一定会先加载内部类,获得一个LazyThree实例,巧妙避开了线程安全问题
        return LazyLoader.Lazy;
    }
    //默认不加载
    private static class LazyLoader{
        private static final LazyThree Lazy=new LazyThree();
    }

总结:本文总结了几种Java中实现单例的方式,其中前两种都不够完美,双重校验锁和静态内部类的方式可以解决大部分问题,平时工作中使用最多的也是这两种方式。

猜你喜欢

转载自blog.csdn.net/fu123123fu/article/details/80031424