1.什么是单例模式?
Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
单例模式就是一个类只有一个对象的实例
2.为什么要用单例模式?
某些应用场景中,有些对象有且只能有一个,如果创建多个就会出现很多问题。例如:一个操作系统中只能打开一个任务管理器,如果可以同时打开多个任务管理器就会出现问题。
3.怎么创建单例模式?
#####方法一:饿汉模式
public class Singleton{
// 1.将构造方法私有化,不允许外部直接创建对象,只能通过类名调用静态方法来获取对象
private Singleton(){
}
//2.创建类的唯一实例,使用private static修饰
private static final Singleton instance = new Singleton();
//3.提供一个用于获取实例的静态方法,使用public static修饰
public static Singleton getInstance(){
return instance;
}
}
创建思路:
(1)如何保证只有一个实例?
方法:将构造方法设置为私有的,这样外部类就无法直接创建类的对象
那么问题来了……
(2)外部类是需要一个唯一实例的,无法创建怎么办?
方法:在类的内部创建类的唯一实例,并声明为static(声明为static的目的是将实例声明为类的成员,这样在其他类中就可以通过类名直接访问这个实例),同时声明为private防止外部直接访问这个实例,但是这样就会导致使用类名调用这个实例不能实现,此时需要创建一个方法来调用这个实例
(3)提供一个用于获取实例的方法,要用static修饰,类方法
#####方法二:懒汉模式
public class Singleton{
// 1.将构造方法私有化,不允许外部直接创建对象
private Singleton(){
}
//2.**声明**类的唯一实例,使用private static修饰(只是声明类的实例并没有实例化)
private static final Singleton instance;
//3.提供一个用于获取实例的静态方法,使用public static修饰
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
创建思路:
(1)同懒汉模式
(2)声明类的唯一实例(只是声明,创建工作留在方法中)
(3)提供一个获取实例的方法(方法中创建实例)
##4.饿汉模式和懒汉模式的区别 ##
饿汉模式的特点是加载类比较慢(因为加载类的时候就已经创建了对象),但是运行时获取对象的速度较快 线程安全
懒汉模式的特点是加载类比较快(加载类的时候没有创建对象),但是运行时获取对象的速度较慢(运行时创建对象) 非线程安全(两个线程同时校验instance非空们就会同时创建instance对象)
##5.改进版:双重锁 ##
public class Singleton{
private static volatile Singleton instance=null;
//volatile关键字:防止jvm优化代码
private Singleton(){
}
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
接着上一篇的单例模式说起,为什么要在多线程中创建单例模式的时候要进行双重锁定?先回顾一下双重锁定的代码块。
public class SingleTon {
private static SingleTon singleTon = null;
public SingleTon() {
// TODO Auto-generated constructor stub
}
public static SingleTon getInstance(){
if (singleTon == null) {
synchronized (SingleTon.class) {
if (singleTon == null) {
singleTon = new SingleTon();
}
}
}
return singleTon;
}
}
为何要使用双重检查锁定呢?
考虑这样一种情况,就是有两个线程同时到达,即同时调用 getInstance() 方法,
此时由于 singleTon == null ,所以很明显,两个线程都可以通过第一重的 singleTon == null ,
进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleTon == null ,
而另外的一个线程则会在 lock 语句的外面等待。
而当第一个线程执行完 new SingleTon()语句后,便会退出锁定区域,此时,第二个线程便可以进入 lock 语句块,
此时,如果没有第二重 singleTon == null 的话,那么第二个线程还是可以调用 new SingleTon ()语句,
这样第二个线程也会创建一个 SingleTon实例,这样也还是违背了单例模式的初衷的,
所以这里必须要使用双重检查锁定。
细心的朋友一定会发现,如果我去掉第一重 singleton == null ,程序还是可以在多线程下完好的运行的,
考虑在没有第一重 singleton == null 的情况下,
当有两个线程同时到达,此时,由于 lock 机制的存在,第一个线程会进入 lock 语句块,并且可以顺利执行 new SingleTon(),
当第一个线程退出 lock 语句块时, singleTon 这个静态变量已不为 null 了,所以当第二个线程进入 lock 时,
还是会被第二重 singleton == null 挡在外面,而无法执行 new Singleton(),
所以在没有第一重 singleton == null 的情况下,也是可以实现单例模式的?那么为什么需要第一重 singleton == null 呢?
这里就涉及一个性能问题了,因为对于单例模式的话,new SingleTon()只需要执行一次就 OK 了,
而如果没有第一重 singleTon == null 的话,每一次有线程进入 getInstance()时,均会执行锁定操作来实现线程同步,
这是非常耗费性能的,而如果我加上第一重 singleTon == null 的话,
那么就只有在第一次,也就是 singleTton ==null 成立时的情况下执行一次锁定以实现线程同步,
而以后的话,便只要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就可以解决由线程同步带来的性能问题了。
为什么要加volatile
首先要理解new Singleton()做了什么。
new一个对象有几个步骤。
1.看class对象是否加载,如果没有就先加载class对象
2.分配内存空间,初始化实例
3.调用构造函数
4.返回地址给引用。
而cpu为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配,就被使用了,进而返回null。
双重锁模式的优点
//这个模式将同步内容放到if内部,提高了执行的效率,不必每次获取对象时都进行同步,只有第一次才同步,创建了以后就没必要了。
//这种模式中双重判断加同步的方式,比第一个例子中的效率大大提升,因为如果单层if判断,在服务器允许的情况下,
//假设有一百个线程,耗费的时间为100*(同步判断时间+if判断时间),而如果双重if判断,100的线程可以同时if判断,理论消耗的时间只有一个if判断的时间。
//所以如果面对高并发的情况,而且采用的是懒汉模式,最好的选择就是双重判断加同步的方式。