单例模式
java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例。
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
package geekgl.factory.method;
/**
* 单例模式--之懒汉模式
*
* 单例模式有以下特点:
*1、单例类只能有一个实例。
*2、单例类必须自己创建自己的唯一实例。
*3、单例类必须给所有其他对象提供这一实例。
*/
public class Singleton {
//构造方法私有化
private Singleton(){} //这里用private 避免了外部实例化这个对象(不能 Singleton instance = new Singleton())
private static Singleton instance = null;
//静态工厂方法
public static Singleton getInstance(){ //这是供外部提供的方法,用来获取该类唯一的实例,如果没有instance用自己私有构造去创建,有就返回
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
至于为什么叫懒汉模式,应该是基于该类逻辑造成的,该类不急着去实例化自己,判断自己实例为空才用自己的私有构造去实例化这个类的对象供外使用。
但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全.
1、在getInstance方法上加同步
public static synchronized Singleton getInstance() {
if (instance == null) {
instance= new Singleton();
}
return instance;
}
在方法上添加锁,的确可以做到线程安全,但是有个问题:
这个同步方法影响的区域太大,如果多个对象想获取这个对象的时候会柱塞在这排队,建议缩小同步区域。
2、双重检查锁定
public static Singleton getInstance() {
if (instance== null) {
synchronized (Singleton.class) {
instance= new Singleton();
}
}
return instance;
}
以上方法缩小了方法区域,可是存在这么一个问题:
当 instance 为 null 时,两个线程可以并发地进入 if 语句内部。然后,一个线程进入 synchronized 块来初始化 singleton ,而另一个线程则被阻断。当第一个线程退出 synchronized 块时,等待着的线程进入并创建另一个 Singleton 对象。注意:当第二个线程进入 synchronized 块时,它并没有检查 instance 是否非 null。 为处理上面的问题,我们需要对 singleton 进行第二次检查。这就是“双重检查锁定”名称的由来。看一下方法;
public static Singleton getInstance() {
if (instance== null) {
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton();
}
}
}
return instance;
}
以上方法保证了同时有多个线程时,我们暂且是说连个线程,哪个线程先得到锁哪个就先创建实例并返回,同步锁下面再次执行实例是否为空判断,保证了获取的同一个对象并没有再去创建(假如说线程一较猛,它先得到锁对象也创建完了,也return了一个单例实例instance,线程二进来直接拿线程一创建并由该方法返回的instance就行了)
我再插一点额外的,如果仅仅是对设计模式的研究暂且忽略线程并发问题可以在此跳过!!
双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。
无序写入带来的问题:
请看以上代码处 instance= new Singleton(); 此行代码创建了一个 Singleton 对象并初始化变量 instance来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的。还是假设有两个线程进入该getInstance方法,线程1执行到instance= new Singleton(); 处,还没执行到下一行也意味着new Singleton()这个构造函数还没执行前使得实例为非null,就在这个时候线程2占了线程一资源,这时候线程2因为instance非null跑到构造器这里获取实例,可是这个获取的是构造完整但是部分已经初始化了的参数,(因为你我都知道单例对象也有除了instance其他的属性的),也就是在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的。最后线程2可能会返回一尚未执行构造函数的对象
详细可以看这里
为了解决这个无序写入的问题:我们可以用volatile保证写入顺序的一致性:
public class SingletonSyn {
private SingletonSyn(){}
private volatile static SingletonSyn instance = null;
//静态工厂方法
public static synchronized SingletonSyn getInstance(){
if(instance == null){
synchronized (SingletonSyn.class){
if(instance == null){
instance = new SingletonSyn();
}
}
}
return instance;
}
}
3、静态内部类
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
一般在单例模式中静态内部类最好的,性能也是最棒的,因为既实现了线程安全,又避免了同步带来的性能影响。
饿汉式单例
/饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton {
private Singleton() {}
private static final Singleton instance = new Singleton();
//静态工厂方法
public static Singleton getInstance() {
return instance ;
}
}
饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。
- 饿汉式和懒汉式区别
从名字上来说,饿汉和懒汉,
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。
另外从以下两点再区分以下这两种方式:
- 1、线程安全:
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。
- 2、资源加载和性能:
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
至于1、2、3这三种实现又有些区别,
第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,
第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。
- 什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。