天天看点

设计模式之美------单例模式【一篇学会单例模式写法】单例模式

单例模式

顾名思义,单例模式的意思就是只有1个实例的对象。就像天天上的太阳和月亮只有一个一样。

单例模式有很多种写法,在这里我们一一对比:

1.懒汉模式

//懒汉模式
public class Singleton1 {
	
	private static Singleton1  instance;
	
	private Singleton1() {};
	
	
	public static Singleton1 getInstance()
	{
		if(instance == null)
		{
			instance = new Singleton1();
		}
		return instance;
	}

}

           

懒汉模式是指在需要的时候再去创建对象。我们可以看到它的成员属性中有一个静态的实例对象,然后对外隐藏了自己的构造方法,是的我们只能通过静态方法getInstance()来获取它的对象。而在getInstance()中,判断了实例对象是否已经被初始化过。

但这个解法的缺点就是只能在单线程模式下工作,在多线程模式下会失效。

我们测试一下,测试代码如下:

public static void main(String[] args) {
	
//线程1尝试获取一个实例
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				Singleton1 s1 = Singleton1.getInstance();
				System.out.println(s1.hashCode());
				
			}
		}).start();
		
		//线程2尝试获取一个实例
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				Singleton1 s2 = Singleton1.getInstance();
				System.out.println(s2.hashCode());
				
			}
		}).start();
		
	}
           

而结果是不稳定的:

设计模式之美------单例模式【一篇学会单例模式写法】单例模式
设计模式之美------单例模式【一篇学会单例模式写法】单例模式

会出现这样两种情况,所以在多线程情况下不推荐使用。

2.饿汉模式

public class Singleton2 
{
	private static Singleton2 instance = new Singleton2();
	private Singleton2() {};
	
	public static Singleton2 getInstance()
	{
		return instance;
	}

}

           

饿汉模式解决了线程安全的问题,但是我们可以看到,因为instance对象是静态的,在类初始化之后,这个对象就已经存在了。可能我们并不需要它,但它仍然会占用内存。

3.加锁模式

public class Singleton3 {
	
	private Singleton3() {};
	static Lock lock = new ReentrantLock();
	private static  Singleton3 instance = null;
	
	private static Object objSingleton3 = new Object();
	
	public static Singleton3 getInstance()
	{
		
	synchronized (objSingleton3) {
		

			if(instance == null)
			{
				instance = new Singleton3();
			
			
			}
		}
	
	return instance;
	}

}
           

这样相比懒汉模式解决了线程安全问题,也不会在不使用的情况下生成对象,但加锁的过程也是较为耗时的。

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

}
           

这种方式和上面的是一样的,就是写法不一样,都是效率比较低。

然后还有一种比较类似的写法,但却是线程不安全的,

public class Singleton4 {
	
	private static Singleton4 instance;
	
	private Singleton4() {};
	
	
	  public static Singleton4 getInstance()
	{
		 if(instance == null)//-------①
		 {
			 synchronized (Singleton4.class) {
				
				 instance = new Singleton4();
			}
			 
		 }
		 return instance;
	}

}
           

这里我们只锁了代码块,但在多线程情况下,线程A可能在①处让出cpu,然后线程B仍然可以获得锁,然后创建对象。当线程A重新拿到CPU的时候,还会继续创建新的对象,这样就不是线程安全的 了。

4.双重检查模式

public class Singleton4 {
	
	private static volatile Singleton4 instance;-------③
	public  volatile static int a=0;
	
	private Singleton4() {};
	
	
	  public static Singleton4 getInstance()
	{
		 if(instance == null)------①
		 {
			 synchronized (Singleton4.class) {
				
				 if(instance == null)-------②
				 {
					
				 instance = new Singleton4();
				 }
			}
			 
		 }
		 return instance;
	}

}
           

在我们进行初始化的时候,检查了两次instance对象是否为空。代码①处的检查是因为要判断instance是否已经存在,避免不必要的上锁。代码②处的检查是因为为了解决并发情况下是否在加锁后再次确认。这种方式相较饿汉式提高了效率,在用到的时候进行初始化,节省了空间,相对于懒汉式能够保证线程安全。而代码③处的volatile关键字保证了instance对象的可见性。避免因为jvm底层命令执行顺序的原因导致出现意料之外的错误。

5.静态内部类

public class Singleton5 {
	
	private Singleton5() {};
	
	public static Singleton5 getInstance()
	{
		return Innerclass.instance;
	}
	
	
	 private static class Innerclass{
		
		
		private static final Singleton5 instance = new Singleton5();
	}

}
           

这个模式结合了饿汉式和懒汉式的优点。它只会在我们第一次使用这个Innerclass的时候,会调用它的构造函数去创建这个属性,如果我们不使用Innerclass,就不会去初始化instance 。

6.枚举法

public enum Singleton6 {
	Singleton6;

}
           

使用的时候直接Singleton6.Singleton6就可以拿到对象。而且枚举类有且只有私有构造器。

而且对序列化也有很好的支持。

总结一下:

单例模式三大要素:

1.私有构造函数:确保不会被外部类访问

2.自己持有,或者通过内部类持有自己类型的实例对象

3.对外暴露一个获取对象的静态方法

对比一下几种方式:

  1. 懒汉模式:只能在单线程中使用
  2. 饿汉模式:创建实例的时机不确定,可能会浪费内存
  3. 加锁模式:线程安全,但会在一定程度上降低代码执行的小绿
  4. 双重检验模式:线程安全,只在第一次创建实例的时候会进行加锁,效率也比较高
  5. 静态内部类:无需加锁,简洁直观,仅在需要的时候创建实例。
  6. 枚举法:简洁方便,自动支持了序列化,但可读性比较低。

综上,在一般开发的时候会比较常用4/5,但也不是唯一的,在不同的场景中可以使用不同的方法才使最好的。

继续阅读