单例模式的七种实现方式

本文详细分析了Java中单例模式的七种实现方式,包括它们的优缺点和适用场景。第一种方式在多线程下线程不安全,第二种方式虽线程安全但效率低下,第三、四种方式在类装载时实例化,第五种方式延迟加载,第六种方式使用枚举实现,提供最强保护,第七种方式为双重检查锁定,适用于JDK1.5及以后版本。文章还指出,类装载器和序列化可能导致多个单例实例的创建,并提供了相应解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
	@Override
	public 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:false
			lineNum 1:true
			lineNum 2:true
			lineNum 3:true
			lineNum 4:true
			lineNum 5:true
			lineNum 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值