设计模式 - 单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
我们可以通过定义一个全局变量给不同的客户端调用,使得不同客户端获取到的都是同一个对象,但是这并不能防止客户端去实例化多个对象,想要保证一个类仅有一个实例,最好的办法就是让类自身保存一个唯一的实例,这个类不仅要能保证客户端不能通过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类中其他的功能,并不需要获取实例,那将会造成内存的浪费。不过像那些在项目中必须要用到的实例,比如加载配置文件的类,就可以使用这种方式。