设计模式——单例模式(详细分析)

设计模式——单例模式(详细分析)

文章目录

  • 设计模式——单例模式(详细分析)
    • 0.概述
    • 1.饿汉式
    • 2.懒汉式
      • 1.一般模式(单线程可用)
      • 2.进阶模式
      • 3.双重检验锁模式⭐(多线程中使用)
    • 3.静态内部类式
    • 4.枚举式

所谓单例模式,就是保证类的对象在内存中唯一存在!

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 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化 。也就是说上面的二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。 我们只需要将 instance 变量声明成volatile 就可以了。

可见:volatile关键字在这里的作用是:禁止指令重排序;

对于volatile关键字,下面一篇博客我会对它进行专门的介绍!

1557123156581

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;
    }
}

这种方式在不同平台有不同的支持度。

你可能感兴趣的:(Java设计模式)