什么是单例模式
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点
单例模式解释起来是很容易的,比如说一个系统只能有一个窗口管理器,Windows只能打开一个任务管理器等等。
那么如何保证一个类只有一个实例,并且这个实例易于被访问呢?
定义一个全局变量可以确保对象随时都可以被访问,但是不能防止我们实例化多个对象。一个更好的解决方法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的动机。
显然单例模式有三个要点
1. 某个类只能有一个实例
2. 它必须自己创建这个实例
3. 它必须自己向整个系统提供这个实例
从具体的实现角度来说,就是
1. 单例模式的类只提供私有的构造函数
2. 类定义中含有一个该类的静态私有对象变量
3. 该类提供一个静态的公有的函数用于创建或获取它本身的静态私有对象
Java实现单例模式
Javai实现单例模式,大致有三种,懒汉式,饿汉式,双重校验锁实现单例模式。第三个毫无疑问是最有趣的一种实现。
饿汉模式
饿汉模式.只要类一加载,我就给你创建好单例对象。想要直接拿。
- 在程序中只能有一个对象,也就是说不能在其他类中任意创建对象,否则对象肯定不止一个,所有我的构造方法不能public,必须是private,就保证了外部不能乱创建我的对象。
public class EagerSimpleton {
/**
* 私有构造函数,保证只能从自身获取自身的对象
*/
private EagerSimpleton(){
}
}
- 外面其他类创建不了对象,那只能我自己辛苦一点,自己创建自己了。再讲究以下java的封装特性,我的对象肯定是private,所以我提供一个public方法供外界去调用。我是饿汉,当然是一加载就初始化出一个实例对象咯
public class EagerSimpleton {
/**
* 私有成员,已初始化
*/
private EagerSimpleton eagerSimpleton = new EagerSimpleton();
/**
* 私有构造函数,保证只能从自身获取自身的对象
*/
private EagerSimpleton(){
}
/**
* 提供public方法供外界调用
* @return
*/
public EagerSimpleton getEagerSimpleton(){
return eagerSimpleton;
}
}
咋一看好像都OK了,可是在外面既然已经不能创建对象了,那还怎么调用你的getEagerSimpleton方法呢,所以这个方法肯定是要加static的,这样就可以通过类名.方法名去访问这个方法去获取对象了。
又因为静态方法内部只能访问静态变量,所以私有化的变量也要加static了。
/**
* @Project:
* @Author: Mr_yao
* @Date: 2019/4/18 1:46 PM
* @Desc: 单例模式-饿汉模式
* 在类加载时就完成了初始化,类加载速度慢,但获取对象速度快
*
*/
public class EagerSimpleton {
/**
* 静态私有成员,已初始化
*/
private static EagerSimpleton eagerSimpleton = new EagerSimpleton();
/**
* 私有构造函数,保证只能从自身获取自身的对象
*/
private EagerSimpleton(){
}
/**
* 静态方法,不用加sysnchorized同步,类加载时已经完成初始化,不存在多线程问题
* @return
*/
public static EagerSimpleton getEagerSimpleton(){
retuirn eagerSimpleton;
}
}
一个简单的饿汉式单例模式也就完成了
同时由于饿汉式是类加载时就已经完成了初始化对象实例的过程,所以在使用 getEagerSimpleton() 的时候是不存在多线程问题的。也就不需要加sysnchorized了。
优点:不需要考虑多线程同步问题
缺点:不管用不用都先创建好,容易造成资源浪费
懒汉式
- 顾名思义,就是非常的懒。我在类加载的时候是不会创建的,你过来问我要,我才给你创建。能偷一点懒是一点懒。
- 这种模式呢,比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢。
/**
* @Project:
* @Author: Mr_yao
* @Date:
* @Desc: 单例模式-懒汉模式
* 比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
*/
public class LazySingleton {
/**
* 静态私有成员,未初始化
* static保证了在自身类中获取自身对象
*/
private static LazySingleton lazySingleton;
/**
* 私有构造函数
* 不能在别的类中获取该类的对象,只能在类自身中获取自己的对象
*/
private LazySingleton(){
}
/**
* public保证对外公开,同步保证多线程时的正确性(因为类不是加载时初始化的)
* @return
*/
public static synchronized LazySingleton getLazySingleton(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
毫无疑问,也是满足我们的单例模式三点要求的。
优点:消耗的资源少,只在用的时候创建
缺点:效率低,在创建对象时初始化工作较多,可能会将这部分时间转嫁给用户,不太友好
双重校验锁实现单例模式
这种模式是最有趣的,直接上代码
/**
* @Project:
* @Author: Mr_yao
* @Date:
* @Desc: 双重校验锁实现对象单例
*/
public class Singleton {
/**
* volatitle
*/
private volatile static Singleton uniqueSingleton;
private Singleton(){
}
public static Singleton getSingleton(){
//先判断有没有实例化,如果没有则进入
if (uniqueSingleton == null){
//类对象加锁
synchronized (Singleton.class){
if (uniqueSingleton == null){
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
- 这和懒汉模式哪里不一样呢
1) 没有在对外提供的获取实例方法上上锁(关于synchronized 我在并发艺术上后续也会详细讲解)
2)自己的静态私有对象用volatile修饰。
3)获取实例方法的代码实现,毫无疑问复杂了不少
那我们仔细看一下修改后的公开获取实例方法。(处于多线程角度思考)
- 第一步,当调用getSingleton()方法时,先看下有没有实例化过,如果有就直接将实例过的那个对象返回,如果没有,那就进入下一步。
if (uniqueSingleton == null)
- 第二步,将类对象上锁,进入临界区。
synchronized (Singleton.class)
- 第三步,我再判断,我有没有被实例化过。(为了防止在第一步判断到第二部进入临界区的过程中,有其他线程对其new 了一个实例)
if (uniqueSingleton == null)
- 第四步,如果我还是没有被实例化,那这个时候我就开始实例化
uniqueSingleton = new Singleton();
这里最重要的一点是,为什么静态私有对象要用volatile去定义呢。
代码上来看,第四步的操作,看起来是一步,其实并不是,它可以分为三步
1). 为Singleton申请一个内存空间,将函数压栈,并且申明变量类型
2). 初始化构造函数以及里面的字段,在堆内存开辟空间
3). 将uniqueSingleton对象指向申请的内存空间
那么Java底层在编译和运行这段代码的时候真的会严格按照这个123步骤去执行么,并不是如此,它会进行指令重排序。
想象一下,现在Singleton并没有实例化过
1. 线程1进入获取实例方法中,第一步->第二步->第三步都通过了,现在进入第四步,但很可惜,第四步中的指令重排序为 1->3->2,它执行了 1 和 3 后,线程2进来了
2. 线程2进来之后,就直接第一步,判断uniqueSingleton == null。
这个时候要说明一下了, "==" 就是判断两个对象指向的内存地址是否一样。即判断两个对象是不是一个对象,基本数据类型判断的 == 比较的是值,引用数据类型 == 比较的是内存地址
那么线程1 已经执行了1 3,所以该对象已经指向了内存空间,不是null。所以线程2在执行第一步的时候,就直接没有走下一步了,美滋滋的拿着返回给它的对象就走了。
毫无疑问,线程2在使用这个没有真正实例化完的对象时,肯定会出问题的。
所以这个时候就需要使用volatile去保证,禁止指令重排序了
我就不再解释volatile了,想要了解volatile直接参考我的另一篇博客即可
多线程并发艺术(三)volatitle
各种实现模式都有优点也有缺点。
推荐使用第三种,双校验锁实现单例模式。