单例模式的七种实现方式

来源:互联网 发布:西方国家为什么不淘宝 编辑:程序博客网 时间:2024/06/15 07:19

import java.util.Random;import java.util.concurrent.*;/** * 第1种方式:懒汉式,线程不安全 */class Singleton1{private static Singleton1 instance;private Singleton1() {}public static Singleton1 getInstance() {if (instance == null) {instance = new Singleton1();}return instance;}}/** * 第2种方式:懒汉式,线程安全 */class Singleton2{private static Singleton2 instance;private Singleton2() {}public static synchronized Singleton2 getInstance() {if (instance == null) {instance = new Singleton2();}return instance;}}/** * 第3种方式:饿汉式,线程安全,基于classloder机制避免了多线程的同步问题 * 在类初始化时即实例化instance */class Singleton3{private static Singleton3 instance = new Singleton3();private Singleton3() {}public static Singleton3 getInstance() {return instance;}}/** * 第4种方式:饿汉式的变种,线程安全 * 跟第3种方式差不多,都是在类初始化时即实例化instance */class Singleton4{private static Singleton4 instance;static {instance = new Singleton4();}private Singleton4() {}public static Singleton4 getInstance() {return instance;}}/** * 第5种方式:静态内部类,线程安全 */class Singleton5{private static class SingletonHolder{private static final Singleton5 instance = new Singleton5();}private Singleton5() {}public static final Singleton5 getInstance() {return SingletonHolder.instance;}}/** * 第6种方式:枚举 */enum Singleton6{INSTANCE;public static Singleton6 getInstance() {return INSTANCE;}}/** * 第7种方式:双重校验锁 */class Singleton7{private static Singleton7 instance;private Singleton7() {}public static Singleton7 getInstance() {if (instance == null) {synchronized (Singleton7.class) {if (instance == null) {instance = new Singleton7();}}}return instance;}}/** * 用于多线程测试的线程类 */class SingletonThread extends Thread{private Object[] objects = new Object[7];private CountDownLatch latch;private CyclicBarrier barrier;@Overridepublic void run() {try {barrier.await();//Thread.sleep(new Random().nextInt(1000));   //输出均为true//Thread.sleep(1000);                         //第1个输出为false,模拟线程不安全的情况} catch (Exception e) {System.out.println(Thread.currentThread().getName());e.printStackTrace();}objects[0] = Singleton1.getInstance();objects[1] = Singleton2.getInstance();objects[2] = Singleton3.getInstance();objects[3] = Singleton4.getInstance();objects[4] = Singleton5.getInstance();objects[5] = Singleton6.getInstance();objects[6] = Singleton7.getInstance();latch.countDown();}public Object[] getObjects() {return objects;}SingletonThread(CountDownLatch latch, CyclicBarrier barrier) {this.latch = latch;this.barrier = barrier;}}/** * 测试类 */public class TestSingleton {public static void main(String[] args) throws Exception {//单线程测试if (false) {isSame(Singleton1.getInstance(), Singleton1.getInstance(), new Throwable().getStackTrace()[0].getLineNumber());isSame(Singleton2.getInstance(), Singleton2.getInstance(), new Throwable().getStackTrace()[0].getLineNumber());isSame(Singleton3.getInstance(), Singleton3.getInstance(), new Throwable().getStackTrace()[0].getLineNumber());isSame(Singleton4.getInstance(), Singleton4.getInstance(), new Throwable().getStackTrace()[0].getLineNumber());isSame(Singleton5.getInstance(), Singleton5.getInstance(), new Throwable().getStackTrace()[0].getLineNumber());isSame(Singleton6.getInstance(), Singleton6.getInstance(), new Throwable().getStackTrace()[0].getLineNumber());isSame(Singleton7.getInstance(), Singleton7.getInstance(), new Throwable().getStackTrace()[0].getLineNumber());}/**多线程测试,输出如下,可以看出第1种方式是非线程安全的lineNum 0:falselineNum 1:truelineNum 2:truelineNum 3:truelineNum 4:truelineNum 5:truelineNum 6:true */if (true) {int ThreadNum = 2;CountDownLatch latch = new CountDownLatch(ThreadNum);CyclicBarrier barrier = new CyclicBarrier(ThreadNum);SingletonThread thread1 = new SingletonThread(latch,barrier);SingletonThread thread2 = new SingletonThread(latch,barrier);thread1.start();thread2.start();latch.await();      //等待子线程执行完毕Object[] objects1 = thread1.getObjects();Object[] objects2 = thread2.getObjects();for (int i = 0; i < 7; i++) {isSame(objects1[i], objects2[i], i);}}}static void isSame(Object a, Object b, int lineNum) {if (a == null || b == null) {System.out.println("a == null || b == null");}else {boolean result = a == b;System.out.println("lineNum " + lineNum + ":" + result);}}}

分析:

  第1种方式:是一种lazy loading的方式,但是致命的缺点是在多线程环境下不能正常工作,上述代码中模拟了多线程环境,从模拟的情况来看确实是线程不安全的。
  第2种方式:这种方式能够在多线程环境中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
  第3种方式:这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
  第4种方式:表面上看起来与第3种方式差别挺大,其实跟第3种方式差不多,都是在类初始化即实例化instance。
  第5种方式:这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第3种和第4种方式不同的是(很细微的差别):第3种和第4种方式是只要Singleton(Singleton3 or Singleton4)类被装载了,那么instance就会被实例化(没有达到lazyloading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有通过显示调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让它延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第3种和第4种方式就显得很合理。
  第6种方式:这种方式是Effective Java作者Josh Bloch所提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
  第7种方式:这个是第2种方式的升级版,俗称双重检查锁定,在JDK1.5之后,双重检查锁定才能够正常达到单例效果。

总结
有两个问题需要注意:
1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
对第1个问题修复的办法是:

private static Class getClass(String classname)                                             throws ClassNotFoundException {         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();             if(classLoader == null)            classLoader = Singleton.class.getClassLoader();             return (classLoader.loadClass(classname));      }   }
对第2个问题修复的办法是:
public class Singleton implements java.io.Serializable {      public static Singleton INSTANCE = new Singleton();          protected Singleton() {            }      private Object readResolve() {               return INSTANCE;         }  } 
  对我来说,我比较喜欢第3种和第5种方式,简单易懂,而且在JVM层实现了线程安全(如果不是多个类加载器环境),一般的情况下,我会使用第3种方式,只有在要明确实现lazy loading效果时才会使用第5种方式,另外,如果涉及到反序列化创建对象时我会试着使用枚举的方式来实现单例,不过,我一直会保证我的程序是线程安全的,而且我永远不会使用第1种和第2种方式,如果有其他特殊的需求,我可能会使用第7种方式,毕竟,JDK1.5已经没有双重检查锁定的问题了。
  不过一般来说,第1种不算单例,第3种和第4种就是一种,如果算两种的话,第5种也可以分开写了。所以说,一般单例都是5种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。

参考:http://cantellow.iteye.com/blog/838473

0 0