Java中单例模式的7种写法 附注释和源码

1. 懒汉式单例模式[不推荐]


/****
 * 懒汉式 单例模式
 * 线程不安全
 *  原理: 延迟加载策略 
 *  在 为空的时候才new 对象 返回  多线程环境下可能出现a线程 b 线程同时调用时判断为空 相互覆盖
 *  在开始加载类的时候对象为空
 *  
* @Description:TODO
* @author:   chen.chao
* @time:2018年10月22日 下午2:41:45
 */
public class Singleton1 {
	private static Singleton1 instance;

	private Singleton1() {
		super();
	}
	public static Singleton1 getInstance(){
		if (instance==null) {
			instance = new Singleton1();
		}
		return instance;
	}
	
}

2. 懒汉式加锁


/***
 * 单例2
 * 懒汉式 线程安全
 * 但 加锁粒度太大 基本上很多情况下不需要同步 所以不要用
 * 
* @Description:TODO
* @author:   chen.chao
* 
* @time:2018年10月22日 下午3:25:43
* 
 */
public class Singleton2 {
	private static Singleton2 instance;  
    private Singleton2 (){}
    public static synchronized Singleton2 getInstance() {  
	    if (instance == null) {  
	        instance = new Singleton2();  
	    }  
    return instance;  
    } 
    
}


3. 饿汉式


/***
 * 单例模式3
 * 
 *  饿汉式 直接在类加载时赋值给单例
 *  
 *  简单 避免同步问题 但是不能确定导致类加载时间
 *  
* @Description:TODO
* @author:   chen.chao
* @time:2018年10月22日 下午4:11:24
 */
public class Singleton3 {

	private static Singleton3 instance = new Singleton3();

	private Singleton3() {
	}

	public static Singleton3 getInstance() {
		return instance;
	}

}

4. 静态代码块


/***
 * 
 * 和第三种差不多
 * 只不过放在了静态代码块中
 * 
 * 此处说明下类加载所有属性 静态 方法 加载顺序
加载过程:

1、JVM会先去方法区中找有没有相应类的.class存在。
如果有,就直接使用;
如果没有,则把相关类的.clss加载到方法区。

2、在.class加载到方法区时,先加载父类再加载子类;
先加载静态内容,再加载非静态内容

3、加载静态内容:

把.class中的所有静态内容加载到方法区下的静态区域内
静态内容加载完成之后,对所有的静态变量进行默认初始化
所有的静态变量默认初始化完成之后,再进行显式初始化
当静态区域下的所有静态变量显式初始化完后,执行静态代码块
4、加载非静态内容:把.class中的所有非静态变量及非静态代码块加载到方法区下的非静态区域内。

5、执行完之后,整个类的加载就完成了。

对于静态方法和非静态方法都是被动调用,即系统不会自动调用执行,
所以用户没有调用时都不执行,主要区别在于静态方法可以直接用类名直接调用(实例化对象也可以),
而非静态方法只能先实例化对象后才能调用。

二、对象的创建过程
1、new一个对象时,在堆内存中开辟一块空间。

2、给开辟的空间分配一个地址。

3、把对象的所有非静态成员加载到所开辟的空间下。

4、所有的非静态成员加载完成之后,对所有非静态成员变量进行默认初始化。

5、所有非静态成员变量默认初始化完成之后,调用构造函数。

6、在构造函数入栈执行时,分为两部分:先执行构造函数中的隐式三步,再执行构造函数中书写的代码。

隐式三步:
   ①执行super()语句
 
   ②显示初始化(对开辟空间下的所有非静态成员变量进行) 

   ③执行构造代码块
7、在整个构造函数执行完并弹栈后,把空间分配的地址赋给引用对象。


 * 
* @Description:TODO

* @author:   chen.chao

* @time:2018年10月22日 下午4:15:30
 */
public class Singleton4 {

	private static Singleton4 instance = null;
	static{
		instance = new Singleton4();
	}
	private Singleton4() {
	}
	

	public static Singleton4 getInstance() {
		return instance;
	}

}

5. 静态内部类



/****
 * 静态内部类 方法
 * 
 * 
 * 这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程
 * 它跟第三种和第四种方式不同的是(很细微的差别):
 * 第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果)
 * 而这种方式是Singleton类被装载了,instance不一定被初始化。
 * 因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时
 * 才会显示装载SingletonHolder类,从而实例化instance。 
 * 想象一下,如果实例化instance很消耗资源,我想让他延迟加载
 * 另外一方面,我不希望在Singleton类加载时就实例化
 * 因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载
 * 那么这个时候实例化instance显然是不合适的
 * 
 * 这个时候,这种方式相比第三和第四种方式就显得很合理。
 * 
 * @Description:TODO
 * 
 * @author: chen.chao
 * 
 * @time:2018年10月23日 下午3:12:47
 */
public class Singleton5 {
	private static class SingletonHolder {
		private static final Singleton5 INSTANCE = new Singleton5();
	}

	private Singleton5() {
	}

	public static final Singleton5 getInstance() {
		return SingletonHolder.INSTANCE;
	}
}

6. 枚举类写法 [Effective Java作者Josh Bloch 提倡的方式]



import java.util.HashMap;
import java.util.Map;

/***
 * 
这种方式是Effective Java作者Josh Bloch 提倡的方式
它不仅能避免多线程同步问题
而且还能防止反序列化重新创建新的对象

其主要使用了枚举的三个特性,自由序列化,线程安全,保证单例
首先,我们都知道enum是由class实现的,
换言之,enum可以实现很多class的内容,
包括可以有member和member function,
这也是我们可以用enum作为一个类来实现单例的基础。
由于enum是通过继承了Enum类实现的,
enum结构不能够作为子类继承其他类,
但是可以用来实现接口。此外,enum类也不能够被继承,
在反编译中,我们会发现该类是final的。
其次,enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,
也为保证单例性做了一个铺垫。
这里展开说下这个private构造器,如果我们不去手写构造器,则会有一个默认的空参构造器,
我们也可以通过给枚举变量参量来实现类的初始化。

这里写一个sessionFactry

* @Description:TODO

* @author:   chen.chao

* @time:2018年10月23日 下午3:18:00
 */
public enum Singleton6 {
	instance;
	private Map<Object, Object> sessionFactory;//由于没有导包,假装他是SessionFactory 
	Singleton6(){
		setSessionFactory(new HashMap<>());
		System.out.println("初始化...");
	}
	public Map<Object, Object> getSessionFactory() {
		return sessionFactory;
	}
	public void setSessionFactory(Map<Object, Object> sessionFactory) {
		this.sessionFactory = sessionFactory;
	}
}


7. 双重校验锁 [企业生产使用]



/***
 * 
 * 双重校验锁
 * 
* @Description:TODO
* @author:   chen.chao
* @time:2018年10月23日 下午3:30:24
 */
public class Singleton7 {
    private volatile static Singleton7 singleton;  
    /***
     * 没有volatile修饰符,可能出现Java中的另一个线程看到个初始化了一半的_instance的情况
     * 但使用了volatile变量后,就能保证先行发生关系(happens-before relationship)。
     * 对于volatile变量_instance,所有的写(write)都将先行发生于读(read),
     * 在Java 5之前不是这样,所以在这之前使用双重检查锁有问题。
     * 现在,有了先行发生的保障(happens-before guarantee),你可以安全地假设其会工作良好。
     * 另外,这不是创建线程安全的单例模式的最好方法,你可以使用枚举实现单例模式,
     * 这种方法在实例创建时提供了内置的线程安全。
     * 我们都知道一个经典的懒加载方式的双重判断单例模式:
 
               instance = new Singleton();  //非原子操作
  
看似简单的一段赋值语句:instance= new Singleton(),但是它并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:

memory =allocate();    //1:分配对象的内存空间 

ctorInstance(memory);  //2:初始化对象 

instance =memory;     //3:设置instance指向刚分配的内存地址 

 

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

memory =allocate();    //1:分配对象的内存空间 

instance =memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化

ctorInstance(memory);  //2:初始化对象

 

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。

在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。
     */
    private Singleton7 (){}  
    public static Singleton7 getInstance() {  
    if (singleton == null) {  
        synchronized (Singleton7.class) {  
        if (singleton == null) {  
            singleton = new Singleton7();  
        }  
        }  
    }  
    return singleton;  
    }  
}


8. 源码地址

已上传GitHub

https://github.com/freshgeek/Singleton/tree/master/JavaBase8/src/top/design/singleton

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木秀林

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值