Java中的单例模式

一、什么是单例模式?

     单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

单例模式有一下特点:
1、单例类只能有一个实例。
2、单例类必须自己自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例

二、Java中的单例模式

      当一个类的实例可以有且只可以一个的时候就需要用到了。为什么只需要有一个呢?有人说是为了节约内存,但这只是单例模式带来的一个好处。只有一个实例确实减少内存占用,可是我认为这不是使用单例模式的理由。我认为使用单例模式的时机是当实例存在多个会引起程序逻辑错误的时候。比如类似有序的号码生成器这样的东西,怎么可以允许一个应用上存在多个呢?

      Java中的单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。

三、单例模式详解

1、饿汉式单例:

//饿汉式单例类,在类初始化时,已经自行实例化
public class Singleton{
	private String name = null;
	// 私有的默认构造方法
	private Singleton() {
		System.out.println("create Singleton");
	}
	// 已经自行实例化
	private static final Singleton instance = new Singleton();
	// 静态工厂方法
	public static Singleton getInstance() {
		return instance;
	}
	public static void testSingleton() {
		System.out.println("test Singleton create");
	}
}

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的。

这是最简单的单例,这种单例最常见,也很可靠!它有个唯一的缺点就是无法完成延迟加载——即当系统还没有用到此单例时,单例就会被加载到内存中。

我们再创建一个测试类,对它进行测试:

public class TestSingleton {
	@Test
	public void test() {
		Singleton.testSingleton();
	}
}

输出结果:

create Singleton
test Singleton create

我们可以注意到,在这个单例中,即使我们没有使用单例类,它还是被创建出来了,这当然是我们所不愿意看到的,所以也就有了以下一种单例。

2.懒汉式单例:

// 懒汉式单例,常用的形式
public class Singleton {
	// 私有的默认构造方法
	private Singleton() {
		System.out.println("create Singleton");
	}
	// 相对于饿汉式单例,这里没有 final 
	private static Singleton instance = null;
	// 静态工厂方法
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
	// 一个类中的普通方法
	public static void testSingleton() {
		System.out.println("test Singleton create");
	}
}

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

上面的单例获取实例时,是需要加上同步的,如果不加上同步,在多线程的环境中,当线程1完成新建单例操作,而在完成赋值操作之前,线程2就可能判断instance为空,此时,线程2也将启动新建单例的操作,那么多个就出现了多个实例被新建,也就违反了我们使用单例模式的初衷了。

我们先使用刚才的那个测试类来测试,是否当系统还没有用到此单例时,单例就会被加载到内存中。

运行结果:

test Singleton create

很显然,懒汉式单例解决掉了饿汉式单例中存在的问题。可以看出,在未使用到单例类时,单例类并不会加载到内存中,只有我们需要使用到他的时候,才会进行实例化。

这种单例解决了单例的延迟加载,但是由于引入了同步的关键字,因此在多线程的环境下,所需的消耗的时间要远远大于第一种单例。我们可以通过一段测试代码来说明这个问题。

public class TestSingleton {
	@Test
	public void test() {
		long beginTime1 = System.currentTimeMillis();
		for (int i = 0; i < 10000000; i++) {
			Singleton.getInstance();
		}
		System.out.println("单例1花费时间:" + (System.currentTimeMillis() - beginTime1));
		long beginTime2 = System.currentTimeMillis();
		for (int i = 0; i < 10000000; i++) {
			Singleton1.getInstance();
		}
		System.out.println("单例2花费时间:" + (System.currentTimeMillis() - beginTime2));
	}
}

运行结果:

create Singleton
单例1花费时间:20
create Singleton
单例2花费时间:40

可以看到,使用第一种单例耗时20ms,第二种单例耗时40ms,性能上存在明显的差异。为了使用延迟加载的功能,而导致单例的性能上存在明显差异,是不是会得不偿失呢?是否可以找到一种更好的解决的办法呢?既可以解决延迟加载,又不至于性能损耗过多,所以,也就有了第三种单例:

3.内部类托管单例:

//内部类托管单例:
public class Singleton3 {
	private Singleton3() {
		System.out.println("create Singleton");
	}

	private static class SingletonHolder {
		private static Singleton3 instance = new Singleton3();
	}

	public static Singleton3 getInstance() {
		return SingletonHolder.instance;
	}
}

在这个单例中,我们通过静态内部类来托管单例,当这个单例被加载时,不会初始化单例类,只有当getInstance方法被调用的时候,才会去加载SingletonHolder,从而才会去初始化instance。并且,单例的加载是在内部类的加载的时候完成的,所以天生对线程友好,而且也不需要synchnoized关键字,可以说是兼具了以上的两个优点。

四、注意

一般来说,上述的单例已经基本可以保证在一个系统中只会存在一个实例了,但是,仍然可能会有其他的情况,导致系统生成多个单例:

         public void test() throws Exception {
		Singleton3 s1 = null;
		Singleton3 s2 = Singleton3.getInstance(); 
		// 1.将实例串行话到文件
		try {
			// 1. 将实力串行化到文件
			  FileOutputStream fos = new  FileOutputStream("singleton.txt"); 
			  ObjectOutputStream oos = new  ObjectOutputStream(fos); 
			  oos.writeObject(s2); 
			  oos.flush();
			  oos.close(); 
			  // 2.
			  FileInputStream fis = new	FileInputStream("singleton.txt"); 
			  ObjectInputStream ois = new ObjectInputStream(fis); 
			  s1 = (Singleton3)ois.readObject();
			  if(s1==s2){ 
				  System.out.println("同一个实例"); 
			  } else {
				  System.out.println("不是同一个实例"); 
			  }
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}											
	}

输出结果:

不是同一个实例

可以看到当我们把单例反序列化后,生成了多个不同的单例类,此时,我们必须在原来的代码中加入readResolve()函数,来阻止它生成新的单例

//阻止生成新的实例    
public Object readResolve(){       
	return SingletonHolder.instance;    
}

五、总结:

其实可以生成单例的方法还有其他的几种,我在此也只是大概说了一下常用的一两种,可能有些地方不是很清楚,只有通过在实际项目中运用才能更好的掌握。

我在实际的项目制作过程中,用得比较多的还是懒汉式单例,简单暴力吧。可能以上讲解会有一些不足之处,仅供参考吧。

原文地址:https://www.cnblogs.com/whyalwaysme/p/4432343.html