方法三:懒汉模式(synchronized修饰的非线程安全)
在今年刚刚进行的校招中,不管是笔试题还是面试的过程中,都出现了单例模式的相关问题,因此我总结了一下。写了下面大概的几种实现方法以及缺点及改进。
一.什么是单例模式?
因程序需要,有时我们只需要某个类同时保留一个对象,不希望有更多对象,此时,我们则应考虑单例模式的设计。
二. 单例模式的特点
1. 单例模式只能有一个实例。
2. 单例类必须创建自己的唯一实例。
3. 单例类必须向其他对象提供这一实例。
三. 单例模式VS静态类
在知道了什么是单例模式后,我想你一定会想到静态类,“既然只使用一个对象,为何不干脆使用静态类?”,这里我会将单例模式和静态类进行一个比较。
1. 单例可以继承和被继承,方法可以被override,而静态方法不可以。
2. 静态方法中产生的对象会在执行后被释放,进而被GC清理,不会一直存在于内存中。
3. 静态类会在第一次运行时初始化,单例模式可以有其他的选择,即可以延迟加载。
4. 基于2, 3条,由于单例对象往往存在于DAO层(例如sessionFactory),如果反复的初始化和释放,则会占用很多资源,而使用单例模式将其常驻于内存可以更加节约资源。
5. 静态方法有更高的访问效率。
6. 单例模式很容易被测试。
几个关于静态类的误解:
误解一:静态方法常驻内存而实例方法不是。
实际上,特殊编写的实例方法可以常驻内存,而静态方法需要不断初始化和释放。
误解二:静态方法在堆(heap)上,实例方法在栈(stack)上。
实际上,都是加载到特殊的不可写的代码内存区域中。
静态类和单例模式情景的选择:
情景一:不需要维持任何状态,仅仅用于全局访问,此时更适合使用静态类。
情景二:需要维持一些特定的状态,此时更适合使用单例模式。
四. 单例模式的实现
方法一:饿汉模式(线程安全的)
/**
*Singleton单例模式,方法一
缺点:该方法只要在ClassLoader下就会提供一个对象的单例。但是美中不足的是:不管该资源是否被请求,它
都会创建一个对象,占用jvm内存。
*/
Class Singleton {
private Singleton() {
}
private static final Singleton singleton1 = new Singleton();
public static Singleton getInstance() {
return singleton;
}
}
方法二:懒汉模式(线程不安全)
/**
*Singleton单例模式,方法二
该方法只有在instance为null的时候才会创建一个实例以避免重复创建。
缺点:
但是,此代码在单线程的时候工作正常,但是在多线程的情况下就会有问题了。
比如,如果两个线程同时运行判断getinstance是否为null的if语句,并且getInstance还没有创建是,那么
两个线程都会创建一个实例。因此需要增加一个同步锁(方法三)
*/
Class Singleton {
private singleton() {}
private static Singleton singleton2 = null;
public static Singleton getInstance() {
if(singleton2 == null){
singleton2 = new Singleton();
}
return singleton2;
}
}
方法三:懒汉模式(synchronized修饰的非线程安全)
但是有一个问题:在每一个线程执行到getInstance()方法时,只有一个线程获得锁,其他线程需要等待。这样就会因为等待锁资源造成系统性能的下降。
/**
*Singleton单例模式,方法三 : 使用同步锁的机制
该实现方法加了同步锁,可以有效防止多线程在执行getInstance方法得到2个对象。
缺点:只有在第一次调用的时候,才会出现生成2个对象,才必须要求同步。
而一旦singleton 不为null,系统依旧花费同步锁开销,有点得不偿失。
*/
class Singleton {
private Singleton() {}
private static Singleton singleton3 = null;
public static synchronized Singleton getInstance() {
if( singleton3 == null) {
singleton3 = new Singleton();
}
return singleton3;
}
}
方法四:懒汉模式(非线程安全)
/**
*Singleton单例模式,方法四:减少锁资源
这种写法减少了锁开销,但是在如下情况,却创建了2个对象:
a:线程1执行到1挂起,线程1认为singleton为null
b:线程2执行到1挂起,线程2认为singleton为null
c:线程1被唤醒执行synchronized块代码,走完创建了一个对象
d:线程2被唤醒执行synchronized块代码,走完创建了另一个对象
所以看出这种写法,并不完美。
*/
class Singleton {
private Singleton() {}
private static Singleton singleton4 = null;
public static Singleton getInstance() {
if(singleton4 == null) { // 1
synchronized(Singleton.class) { // 2
singleton4 == new Singleton(); // 3
}
return singleton;
}
}
}
方法五:懒汉模式——双重校验(单核CPU下是线程安全的)
/**
*singleton单例模式,方法五:解决方法四中存在的问题,引入双重检查锁定
在同步锁代码块内部,再判断一次对象是否为null,为null才创建对象。这种写法已经接近完美:
a:线程1执行到1,已经进入synchronized的时候,线程挂起,线程1占有Singleton.class资源锁;
b:线程2执行到1,当它准备synchronized块时,因为Singleton.class被占用,线程2阻塞;
c:线程1被唤醒,判断出对象为null,执行完创建一个对象
d:线程2被唤醒,判断出对象不为null,不执行创建语句
如此分析,发现似乎没问题。
但是实际上并不能保证它在单处理器或多处理器上正确运行;
问题就出现在singleton = new Singleton()这一行代码。它可以简单的分成如下三个步骤:
mem= singleton();//1
instance = mem;//2
ctorSingleton(instance);//3
这行代码先在内存开辟空间,赋给singleton的引用,然后执行new 初始化数据,但是注意初始化是要消耗时间。
如果此时线程3在执行步骤1的时候,发现singleton 为非null,就直接返回,那么线程3返回的其实是一个没构造
完成的对象。
我们期望1,2,3 按照反序执行,但是实际jvm内存模型,并没有明确的有序指定。
这归咎于java的平台的内存模型允许“无序写入”。
*/
class Singleton {
private Singleton() {}
private static Singleton singleton5 = null;
public static Singleton getInstance() {
if(singleton5 == null) { // 1
synchronized(Singleton.class) { // 2
if(singleton5 == null)//3
singleton5 == new Singleton(); // 4
}
return singleton;
}
}
}
方法六:懒汉模式(线程安全)
所以,为了解决在多核CPU下的指令重排问题造成的线程不安全,需要使用volatile关键字修饰Singleton对象,这样可以防止指令重排问题。(但是在单核CPU下,方法五的double-check(双重校验)方式就是线程安全的)
/**
*singleton单例模式,方法六:在方法五的基础上引入volatile
(此方法目前还很模糊)
*/
class Singleton {
private Singleton() {}
private static volatile Singleton singleton5 = null;
public static Singleton getInstance() {
if(singleton5 == null) { // 1
synchronized(Singleton.class) { // 2
if(singleton5 == null)//3
singleton5 == new Singleton(); // 4
}
return singleton;
}
}
}
总结:不推荐使用饿汉模式,因为程序在加载的时候就会去创建类的实例,这样会占用很大的内存空间。推荐使用懒汉模式。
五. 枚举类型的单例模式解决反射暴力破解问题
目前,我们在上面的方法六中已经得出一个看上去很安全的单例模式程序。但是如果使用反射的形式就会发现,通过发射能够暴力破解方法六中的单例安全性。如下代码:
import java.lang.reflect.Constructor;
public class TestReflectClass {
public static void main(String[] args) throws Exception, SecurityException {
// TODO Auto-generated method stub
System.out.println("单例模式:");
System.out.println(singleton4.getInstance().hashCode());
//通过反射获取构造函数,然后得到一个新的实例
Class<?> class1 = singleton4.class;
Constructor<?> constructor = class1.getDeclaredConstructor();
constructor.setAccessible(true);
Object instance = constructor.newInstance();
System.out.println("反射暴力破解单例模式后创建新的实例:");
System.out.println(instance.hashCode());
}
}
以上代码是反射写的获取单例的构造函数,通过构造函数的newInstance()得到一个新的实例。运行结果如下:
可以看出,得到的两个实例是完全不一样的,这也就不符合单例模式的性质。
因此,可以通过创建一个枚举,通过枚举类中的值创建一个内部类实例。如下代码:
/**
* 步骤: 1. 创建一个枚举类,
* 2. 设置一个枚举值INSTANCE
* 3. 创建一个内部类
* 4. 枚举类构造函数
* @author yangguang
*2018年7月18日
*/
public enum singleton5 {
INSTANCE;
private InnerClass _instance;
private singleton5() {
_instance = new InnerClass();
}
public InnerClass getInstance() {
return _instance;
}
private class InnerClass{
}
}
主要是枚举类型是值类型的数据,所以不可能获取到构造函数,也就无法通过构造函数来得到新的实例。这样可以实现很安全的单例模式。(目前很多使用枚举实现单例模式)