设计模式 - 单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

我们可以通过定义一个全局变量给不同的客户端调用,使得不同客户端获取到的都是同一个对象,但是这并不能防止客户端去实例化多个对象,想要保证一个类仅有一个实例,最好的办法就是让类自身保存一个唯一的实例,这个类不仅要能保证客户端不能通过new来创建该类一个实例,即用private来修饰构造方法,还要提供一个获取该类唯一实例的方法。从这几个条件我们可以给出单例类的一种实现方式,代码如下

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

以上这种实现方法有一个明显的缺陷,就是在多线程的情况下将不能保证只实例化一个对象,因为判断singleton为null和初始化singleton并不是原子操作,可能在初始化singleton前有多个线程执行了条件判断,这几个线程就都会进入到if内,这几个线程都会新建一个实例,违背了定义中仅有一个实例的原则,通过以下测试代码来验证这一缺陷

Set<Singleton> singletons = Collections.synchronizedSet(new HashSet<>());
ExecutorService es = Executors.newCachedThreadPool();
CyclicBarrier barrier = new CyclicBarrier(1000);
for (int i = 0; i < 1000; i++) {
    es.execute(() -> {
        try {
            barrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
        singletons.add(Singleton.getInstance());
    });
}
Thread.sleep(5000);
for (Singleton singleton : singletons) {
    System.out.println(singleton);
}

运行以上代码有可能会输出如下图所示的结果

多个线程同时调用单例类中获取实例的方法,并存到一个Set集合中进行去重后获取到了三个不同的实例(也有可能是一个或者更多个)

为了避免多线程下会出现多个实例的问题,可以在getInstance方法上增加synchronized,不过这样虽然能解决上述问题,但是当某个线程在执行该方法的时候,其他的线程都将处于阻塞状态,这会严重影响代码的效率,我们可以在方法中使用同步代码块来减少同步的代码量,以下是改造后的getInstance方法

public static Singleton getInstance() {
    if (singleton == null) {
        synchronized (Singleton.class) {
            if (singleton == null) {
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

 以上代码中使用了两层判断,第一层判断是在同步代码块以外,当已经初始化好一个实例时,多个线程就能一起获取该实例,而第二层判断是在同步代码块内,是让那些通过第一层判断的线程同步执行,避免出现实例化多个对象的问题。这种加锁的方法看上去没有问题,但是JVM在创建一个新对象的时候是需要三个步骤的

1、分配内存

2、初始化构造器

3、将对象指向分配的内存的地址

若按以上步骤执行也不会有问题,但是JVM会针对字节码进行调优,有可能会将2,3对调执行,那就有可能会出现一个线程还未初始化构造器的时候,另一个线程获取了该实例并使用,将会出现错误信息。当然这种情况也只是可能会发生,可以通过volatile来修饰属性singleton,禁止JVM指令重排序优化。

还有一种使用内部类的方法来实现单例模式,代码如下

public class Singleton {

    private Singleton() {
    }

    public static Singleton getInstance() {
        return InnerSingleton.singleton;
    }

    private static class InnerSingleton {
        static Singleton singleton = new Singleton();
    }
}

一个类的静态属性只会在第一次加载类的时候初始化,所以singleton是单例的,并且在初始化完之前其他线程是无法被调用的。

还有一种饿汉式加载的实现方法

public class Singleton {

    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

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

这种方法在第一次加载类的时候就会初始化好一个单例,若我们只想使用Singleton类中其他的功能,并不需要获取实例,那将会造成内存的浪费。不过像那些在项目中必须要用到的实例,比如加载配置文件的类,就可以使用这种方式。

完整代码

posted @ 2019-03-08 09:20  回忆成长  阅读(208)  评论(0编辑  收藏  举报