单例模式(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:
某些类创建比较频繁,对一些大型的对象,这是一笔很大的开销。
省去了new操作符,降低了系统内存的使用频率,减轻GC的压力。
有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以多个创建的话,系统完全乱了。(比如一个军队有多个司令),所以只有使用单例模式,才能保证核心交易服务器独立控制着整个流程。
单例模式的实现方式有两种,饿汉式和懒汉式,饿汉式是在类加载的时候就初始化了实例,它们的优缺点如下:
饿汉式优点: 在多线程模式下是安全的
缺点: 没有调用方法前就被加载,会占用内存
懒汉式优点:只有调用方法才创建对象,不会占用内存
缺点:在多线程模式下不安全
当然下面的例子会提供懒汉式的线、线程安全问题的解决办法,让我们一起来学习吧。
1、饿汉式单例模式实例
/**
* 线程安全的饿汉式单利模式,虽然线程安全,但是没有调用方法前就被加载,会占用内存
* 所以懒汉式和饿汉式都有优缺点
* 饿汉式优点: 在多线程模式下是安全的
* 缺点: 没有调用方法前就被加载,会占用内存
* 懒汉式优点:只有调用方法才创建对象,不会占用内存
* 缺点:在多线程模式下不安全(这个要解决)
* @author suibibk.com
*/
public class FirstSingleton{
private static FirstSingleton singleton = new FirstSingleton();
private FirstSingleton() {
System.out.println("创建实例成功");
}
public static FirstSingleton getInstance() {
System.out.println("调用单例模式的方法");
return singleton;
}
public static void main(String[] args) {
FirstSingleton.getInstance();
}
}
运行上面例子,会发现先打印的是:创建实例成功,后打印的是:调用单例模式的方法,表明 没有调用方法前就被加载。如果我们不考虑内存占用,用这种模式是最好的,但是如果要考虑内存占用,就只能够用懒汉式啦。
2、懒汉式-简单懒汉式
/**
* 懒汉式单例模式,第一种:这种只是简单的符合饿汉式单例模式,在高并发的情况下线程不安全
* @author suibibk.com
*/
public class SecondSingleton{
private static SecondSingleton singleton = null;
private SecondSingleton() {
System.out.println("创建实例成功");
}
public static SecondSingleton getInstance() {
System.out.println("调用单例模式的方法");
if(singleton==null) {
singleton = new SecondSingleton();
}
return singleton;
}
public static void main(String[] args) {
SecondSingleton.getInstance();
}
}
运行例子会先打印:调用单例模式的方法,后打印创建实例成功,但是这种会有线程安全问题。
3、懒汉式-同步方法懒汉式
/**
* 懒汉式单例模式,在getInstance方法上加锁,解决线程安全问题
* synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,
* 因为每次调用getInstance(),都要对对象上锁,事实上,
* 只有在第一次创建对象的时候需要加上锁,之后就不需要了,所以这个地方需要改进。
* @author suibibk.com
*
*/
public class SecondSingleton2{
private static SecondSingleton2 singleton = null;
private SecondSingleton2() {
System.out.println("创建实例成功");
}
public static synchronized SecondSingleton2 getInstance() {
System.out.println("调用单例模式的方法");
if(singleton==null) {
singleton = new SecondSingleton2();
}
return singleton;
}
public static void main(String[] args) {
SecondSingleton2.getInstance();
}
}
运行例子会先打印:调用单例模式的方法,后打印创建实例成功,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加上锁,之后就不需要了,所以这个地方需要改进。
4、懒汉式-同步双重检验机制模式
/**
* 懒汉式单例模式,对加锁后的改进方法1,用双重检验机制,提高效率;
* 但是在Java指令中创建对象和赋值操作是分开进行的,也就是说singleton = new SecondSingleton3();
* 语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,
* 然后直接赋值给singleton成功,然后再去初始化这个SecondSingleton3实例。这样就有可能出错了
* @author suibibk.com
*/
public class SecondSingleton3{
private static SecondSingleton3 singleton = null;
private SecondSingleton3() {
System.out.println("创建实例成功");
}
public static SecondSingleton3 getInstance() {
System.out.println("调用单例模式的方法");
if(singleton==null) {
synchronized(SecondSingleton3.class) {
if(singleton==null) {
singleton = new SecondSingleton3();
}
}
}
return singleton;
}
public static void main(String[] args) {
SecondSingleton3.getInstance();
}
}
运行例子会先打印:调用单例模式的方法,后打印创建实例成功,一般来说,做到上面这一步,基本上都会认为已经完美解决了问题,将synchronized关键字加在了内部也就说当调用的时候是不需要加锁的,只有在singleton为null,并创建对象的时候才需要加锁,并且用双重检查机制,性能有一定的提升。
但是,这样的情况,可能还是有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说singleton = new SecondSingleton3();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的SecondSingleton3实例分配空间,然后直接赋值给singleton成功,然后再去初始化这个SecondSingleton3实例。这样就有可能出错了,我们以A,B两个线程为例:
A、B线程同时进入了第一个if判断;
A首先进入synchronized块,由于singleton为null,所以它执行new SecondSingleton3();
由于JVM的内部优化机制,JVM先画出了一些分配给SecondSingleton3实例的空白内存,并复赋值给singleton成员(此时,JVM并没有初始化这个实例),然后A离开了synchronized块。
B进入synchronized块,由于singleton此时不是null,因此它马上离开synchronized块并将结果返回给调用程序。
此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。(此时A还在初始化对象)
所以程序还是有可能发生错误的,其实程序在运行过程很复杂,从这点我们可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们应该对程序做进一步优化。
5、懒汉式-内部类模式
/**
* 懒汉式单例模式,懒汉式单例模式最终解决方案
* 使用内部类来维护单例的实现,JVM内部机制能够保证一个类在被加载的时候,这个类的加载过程是线程互斥的。
* @author suibibk.com
*/
public class SecondSingleton4{
private SecondSingleton4() {
System.out.println("创建实例成功");
}
private static class SingleFactory{
private static SecondSingleton4 instance = new SecondSingleton4();
}
public static SecondSingleton4 getInstance() {
System.out.println("调用单例模式的方法");
return SingleFactory.instance;
}
public static void main(String[] args) {
SecondSingleton4.getInstance();
}
}
运行例子会先打印:调用单例模式的方法,后打印创建实例成功,单例模式使用内部类来维护单例的实现,JVM内部机制能够保证一个类在被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能的问题。这样我们暂时总结了一个完美的单例模式。
其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。
总结
任何技术都要考虑到方方面面,我们这其实只需要做到双重检查哪个机制就可以了。