1、什么是单例模式
单例模式,应该是最简单的设计模式,在类图上只有一个类。那么这个最简单的设计模式到底有什么用呢?
单例模式的作用:
确保一个类只有一个实例,并提供一个全局访问点。
单例模式的特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例
3、单例类必须给所有其他对象提供这一实例。
单例模式的具体应用场景:
在一些应用中,有一些对象其实我们只需要一个,比如:线程池(ThreadPool),缓存(cache),对话框,处理偏好设置和注册表的对象,日志对象,充当显示器和驱动程序的对象。如果在这类对象有多个实例,就会产生很多问题。比如资源使用过量,或者程序行为异常等。
2、有哪些单例的实现方式
1、lazy initialization, thread-unsafety(懒汉法,线程不安全)
public class Singleton{
//利用一个静态变量来记录Singleton类的唯一实例
private static Singleton instance = null;
//将无参的构造方法设置为private,是为了避免类在外部被实例(new Singleton())
private Singleton(){};
//通过getInstance方法来实例化对象,并返回这个实例
public static Singleton getInstance(){
if(instance == null ){
instance = new Singleton();
}
return instance;
}
}
2、lazy initialization, thread-safety(懒汉法,线程安全)
public class Singleton{
private static Singleton instance = null;
private Singleton(){};
public static synchronized Singleton getInstance(){
if(instance == null ){
instance = new Singleton();
}
return instance;
}
}
这种写法也很简单,直接在getInstance()方法上加上synchronized关键字,这样就线程安全了。不过这样写也有一个坏处,那就是虽然线程安全了,但是在不需要考虑多线程的时候效率低下。所以有了另一种方法 double-checked locking(双重检查加锁)。
3、double-checked locking(双重检查加锁)
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
那么他是如何工作的哪?
假设现在有两个线程A,B同时执行到了getInstance()方法,那么他们就会进入if判断,结果instance == null成立,同时进入if语句,这个时候遇到了synchronized 同步,则只有一个线程会执行synchronized内的代码,假设线程A执行内部代码,则这是B会进入等待,当A进入synchronized内部的代码,判断instance为空成立,则创建一个Singleton象,并赋值给instance。这时A执行完synchronized内的代码,交出同步锁,B进入内部代码,这时instance已被赋值,则判断为false,直接退出,这时A,B都获得了同一Singleton对象。
----------重点来了----------
volatile的作用:假设没有关键字volatile的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行instance = new Instance(),该构造方法是一个非原子操作,编译后生成多条字节码指令,由于JAVA的指令重排序,可能会先执行instance的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后instance便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B进入,就会看到一个不为空的但是不完整(没有完成初始化)的Instance对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。(可惜,这个并不是自己想到的。。。。)
---------------------------------------------------我是分割线----------------------------------------------
上面的都是懒汉法,下面带来饿汉法。
懒汉法和饿汉法的区别就在于是否延迟初始化。
4、eager initialization thread-safety (饿汉法,线程安全)
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
“饿汉法”就是在使用该变量之前就将该变量进行初始化,这当然也就是线程安全的了。
在该类进行加载的时候就会初始化该对象,而不管是否需要该对象。这么写的好处是编写简单,而且是线程安全的,但是这时候初始化instance显然没有达到lazy loading的
效果
。会在成内存的浪费。
5、static inner class thread-safety (静态内部类,线程安全)
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
这种写法利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗。(应该是最好的写法)
3、后记
原本以为3这种双重检查加锁的方法已经很好了,结果看到一篇大牛的文章发现,这种方法中种方法在同步时还是有问题的。(当时那个震惊啊……)
----------------------------------大牛解释--------------------------------
经过刚才的分析,貌似上述双重加锁的示例看起来是没有问题了,但如果再进一步深入考虑的话,其实仍然是有问题的。
如果我们深入到JVM中去探索上面这段代码,它就有可能(注意,只是有可能)是有问题的。
因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,
如果我们深入到JVM中去探索上面这段代码,它就有可能(注意,只是有可能)是有问题的。
因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,
但是有些情况下是会造成莫名的错误。
首先要明白在JVM创建新的对象时,主要要经过三步。
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的
首先要明白在JVM创建新的对象时,主要要经过三步。
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的
(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了。
因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给synchronizedSingleton,然后再进行初始化构造器,
因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给synchronizedSingleton,然后再进行初始化构造器,
这时候后面的线程去请求getInstance方法时,会认为synchronizedSingleton对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,
这个线程使用了synchronizedSingleton,就会产生莫名的错误。
---------------------心里默念:我是菜鸟,我是菜鸟 -------------------
最好让我们一起喊出我们的口号:我是菜鸟,我啥都不会!!!