设计模式——单例模式(详细分析)
所谓单例模式,就是保证类的对象在内存中唯一存在!
0.概述
单例的三大要点:
- 线程安全
- 延迟加载
- 序列化与反序列化安全
1.饿汉式
饿汉式单例模式是最基础的,这里我就不再过多讲解;
特点:
- 一上来就new,所以叫饿汉式
- 饿汉式是线程安全的(因为使用final修饰,所以这里只能由一个实例化对象,且不能被修改)
class Singleleton {
//注意这里必须是private static final
private static final Singleleton single = new Singleleton();
private Singleleton() { }
public static Singleleton getSingle(){
return single;
}
}
2.懒汉式
1.一般模式(单线程可用)
所谓懒汉式,就是等到调用获取单例方法时再new对象进行返回;
特点:
- 这种写法与饿汉式相比,有着延时加载的特点,也尽可能的节省了内存空间;
- 但这种写法有一个致命缺点就是线程不安全 ,当同时有多个线程进入到getSingle方法时,就会产生了多个实例化对象;
class Singleleton {
private static Singleleton single = null;
private Singleleton() { }
public static Singleleton getSingle(){
if(single == null) {
single = new Singleleton();
}
return single;
}
}
2.进阶模式
下面的代码就是对上面的进行了加锁设置,这样一来,当多线程访问时,就只能有一个线程对其访问,从而保证了单个实例的产生;
特点:
- 当多线程访问时,只能有一个线程对其访问,所以其他线程都得在此等待锁的释放,这样一来,导致效率极其低下;(本来单例模式就是绝大多数线程来读取单例对象,全部在那等待锁会让效率大打折扣) ;
class Singleleton {
private static volatile Singleleton single = null;
private Singleleton() { }
public static Singleleton getSingle(){
synchronized (Singleleton.class) {
if(single == null) {
single = new Singleleton();
}
}
return single;
}
}
3.双重检验锁模式⭐(多线程中使用)
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。
为什么要在同步块外加一次判断?
因为加上这次判断,就会解决上面那个版本的问题,这样一来,很多线程访问时就不用再等待锁,这样极大的提高了效率;
为什么在同步块内还要再检验一次?
因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了
看似多此一举,但实际上却极大提升了并发度,进而提升了性能 !!!!!!!!!!
class Singleleton {
private static volatile Singleleton single = null;
private Singleleton() { }
public static Singleleton getSingle(){
if(single == null) {
synchronized (Singleleton.class) {
if(single == null) {
single = new Singleleton();
}
}
}
return single;
}
}
在这里我再着重讲解一下为啥要在定义的时候加上volatile关键字 :(jdk5后)
这段代码看起来很完美,很可惜,它是有题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化 。也就是说上面的二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。 我们只需要将 instance 变量声明成volatile 就可以了。
可见:volatile关键字在这里的作用是:禁止指令重排序; ⭐
对于volatile关键字,下面一篇博客我会对它进行专门的介绍!
3.静态内部类式
那么,有没有一种延时加载,并且能保证线程安全的简单写法呢?我们可以把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次 ,所以这种写法也是线程安全的:
class Singleleton {
private static class Inner {
static Singleleton single = new Singleleton();
}
private Singleleton() { }
public static Singleleton getSingle(){
return Inner.single;
}
}
其实这种写法和饿汉式是大同小异;
4.枚举式
对于上面三种方式而言,都还存在着一点点缺陷:
- 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
- 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
这里提出了一种新的方法产生单例,使用枚举!!!
public enum Singleton {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
这种方式在不同平台有不同的支持度。