前言
这篇 博客是跑更问底的学习单例模式,看了本博客,对于一般的面试官,你都可以手撕了,但是大神级别的面试官,后边还会补充。
一、手写单例模式
1、饿汉式
public class Singleton {
// 构造方法私有化,其他类就不能通过new的方式来创造对象
private Singleton(){
}
// 内部提供一个当前的实例,必须要静态化,因为下面的静态方法要调用
private static Singleton singleton=new Singleton();
// 提供公共的静态方法,返回当前类的对象,外部类调用的唯一路径
public static Singleton getInstance(){
return singleton;
}
}
2、懒汉式
public class Singleton {
private Singleton() {
}
private static Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
3、对比分析
首先饿汉式,就是迫不及待的new 出一个对象,然后不管你调不调用,我都要new 出一个对象,这样虽说是线程安全的,但是对象加载的时间长,耗费内存。 懒汉式,因为它是懒加载,什么时候用,什么时候new 对象,延时了对象的创建,节省内存空间,但是它是线程不安全的。如果不知道为啥是线程不安全的,还请看我之前写的多线程的系列博客。
4、jdk中单例模式应用举例
jdk中的RunTime 就是饿汉式的 ,如下:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
二、饿汉式线程不安全的解决方式
(1)synchronized关键字实现线程同步
public class Singleton {
private Singleton() {
System.out.println("hahahaha");
}
private static Singleton singleton = null;
public static Singleton getInstance() {
synchronized (Singleton.class){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
}
上面的代码效率稍差,先不提synchronized()重量级锁的事情,是因为当singleton不是null的时候,其他线程每次进来的时候都要去判断有没有Singleton.class这个锁,效率差在了这里,我们稍微改进一下,用双端检索机制(DCL (double check lock))进行修改.
(2) DCL 方式提高效率
public class Singleton {
private Singleton() {
System.out.println("hahahaha");
}
private static Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
到这里你就觉得完美了,重点来了,上面的双端监测的方式也不一定是线程安全的,因为还有指令重排的存在。这会导致某一个线程执行到第一次检测,读取到的singleton不为null时,singleton的引用对象可能没有完成初始化
因为singleton=new Singleton();可以分为三步
伪代码
memory=allocate(); //1、分配对象内存空间
singleton(memory);//2、初始化对象
singleton=memory;//3、设置singleton指向刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.
memory=allocate();//1.分配对象内存空间
singleton=memory;//3.设置singleton指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
singleton(memory);//2.初始化对象
由上可知,当一条线程访问singleton不为null时,由于存在singleton实例未完成初始化的可能性,此时就造成了线程安全问题。
既然是指令重排导致的问题,我们前边介绍了volatile关键字的作用了, 看一看这篇博客 https://blog.csdn.net/jerry11112/article/details/106870835
,其中volatile可以保证原子性,禁止指令重排,所以我们加上volatile关键字就好
public class Singleton {
private Singleton() {
}
private static volatile Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
三、单例模式——应用场景
1、应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只有一个实例去操作,否则内容不好追加。
2、数据库连接池
3、网站计数器
4、Spring中的单例模式,Spirng bean有一个属性为scope 其中默认就是singleton 就是单例模式