java单例模式singleton方法总结

 

 

摘要:

一.什么是单例模式?

二. 单例模式的特点

三. 单例模式VS静态类

四. 单例模式的实现

方法一:饿汉模式(线程安全的)

方法二:懒汉模式(线程不安全)

方法三:懒汉模式(synchronized修饰的非线程安全)

方法四:懒汉模式(非线程安全)

方法五:懒汉模式——双重校验(单核CPU下是线程安全的)

方法六:懒汉模式(线程安全)

五. 枚举类型的单例模式解决反射暴力破解问题


在今年刚刚进行的校招中,不管是笔试题还是面试的过程中,都出现了单例模式的相关问题,因此我总结了一下。写了下面大概的几种实现方法以及缺点及改进。

一.什么是单例模式?

 

因程序需要,有时我们只需要某个类同时保留一个对象,不希望有更多对象,此时,我们则应考虑单例模式的设计。

二. 单例模式的特点

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{
		
	}
	
}

主要是枚举类型是值类型的数据,所以不可能获取到构造函数,也就无法通过构造函数来得到新的实例。这样可以实现很安全的单例模式。(目前很多使用枚举实现单例模式)

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值