单例模式

1-单例模式

1-1 什么是单例模式

单列模式是为了保证某个对象在程序的生命周期内,在内存中只存在一个实例。

即一个类只有一个对象。

尽量在合适的场合使用单例

使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:
控制资源的使用,通过线程同步来控制资源的并发访问
控制实例的产生,以达到节约资源的目的
控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信

1-2 单例模式优点

  • 1-减少实例的创建
  • 2-减少垃圾回收
  • 3-缓存快速获取
首先是节省内存资源,
对于程序中一些大对象,这些对象占用内存较大,
频繁创建不但内存开销增大,还会耗费很多内存 分配的时间,
另外,会触发频繁的 GC(垃圾回收),GC 又会增加时间开销,情况严重的,可能还会发生 Full GC, 导致 程序主线程阻塞,

1-3 单例模式的缺点

如果单例有状态的话,在多线程下,可能线程不安全。

1-4 使用注意

1-不要在controller中定义成员变量。

2-万一必须要定义一个非静态成员变量时候,则通过注解@Scope(“prototype”),将其设置为多例模式。

3-在Controller中使用ThreadLocal变量

2-单例模式的实现

2-1 饿汉方式单例

通过类加载来保证线程安全

这种方式是基于 java 类加载机制避免了多线程同步的问题,

不过正因为如此,

实例在装载时就实例化,

而且导致实例化的 原因还有很多种,

有时候,我们是不愿意将其实例化的。

而且单例模式大多数实例化都是调用 getInstance() 方法,违背 了懒加载的设计。

class Singleton1{


    /*
    有多饿,一上来就实例化

    初始化的时刻:
    1-after
    2-读静态字段
    3-设置一个静态字段
    4-静态方法
     */
    private static Singleton1 instance = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return instance;
    }
}

2-2 饿汉模式的变种

通过类加载来保证线程安全

利用了 static 块来实例化对象

使用static来定义静态成员变量或静态代码,

借助Class的类加载机制实现线程安全单例。

class Singleton2 {

    private static Singleton2 instance = null;

    static {
        instance = new Singleton2();
    }

    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        return instance;
    }
}

2-3 静态内部类来实现单例

3-通过静态内部类来实现单例
通过类加载来保证线程安全
使用了lazy-loading。
Singleton类被装载了,但是instance并没有立即初始化。
因为 SingletonHolder类没有被主动使用,
只有显式通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化 instance。
是不是很妙啊

class Singleton3 {

    private static class SingletonHolder {
        private static final Singleton3 INSTANCE = new Singleton3();
    }

    private Singleton3() {
    }

    public static final Singleton3 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

2-4 使用枚举实现单例

4-使用枚举
线程安全
它不仅能避免多线程同步问题,
而且还能防止反序列化重新创建新的对 象,
可谓是很坚强的壁垒。
enum Singleton4 {

    INSTANCE;

    public void whateverMethod() {
    }
}

2-5 CAS实现单例

5-使用cas的思想,来保证单例
CAS是项乐观锁技术,
当多个线程尝试使用CAS同时更新同一个变量时,
只有其中一个线程能更新变量的值,而其它线程都失 败,
失败的线程并不会被挂起,而是被告知这次竞争中失败,
并可以再次尝试
有什么缺点呢:
用CAS的好处在于不需要使用传统的锁机制来保证线程安全,
CAS是一种基于忙等待的算法,依赖底层硬件的实现,
相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),
会对CPU造成较大的执行开销。
class Singleton5 {

    // 原子性
    private static final AtomicReference<Singleton5> INSTANCE = new AtomicReference<Singleton5>();

    private Singleton5() {}

    public static Singleton5 getInstance() {
        // 死循环
        while (1 == 1) {
            Singleton5 singleton5 = INSTANCE.get();
            if (null != singleton5) {
                return singleton5;
            }
            // 为空
            // 如果N个线程同时执行到singleton = after Singleton();的时候,
            // 会有大量对象创建,很可能导致内存溢出。
            singleton5 = new Singleton5();
            if (INSTANCE.compareAndSet(null, singleton5)) {
                return singleton5;
            }
        }
    }
}

2-6 懒汉单例,线程不安全

class Singleton6 {
    private static Singleton6 instance;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            // 比较致命的是,在多线程情况下,会出现多次实例化 Singleton 对象
            instance = new Singleton6();
        }
        return instance;
    }
}

2-7 懒汉,懒加载单例

7-懒汉
* 懒加载,
* 要的时候,才new
* 线程安全的单例,但是效率低
class Singleton7 {
    private static Singleton7 instance;

    private Singleton7() {
    }

    // 这里加锁了,锁类,效率低,
    public static synchronized Singleton7 getInstance() {
        if (instance == null) {
            instance = new Singleton7();
        }
        return instance;
    }
}

2-8 双重校验锁创建单例

通过在 synchronized 的外面增加一层判断,就可以在对象一经创建以 后,
不再进入 synchronized 同步块。
这种方案不仅减小了锁的粒度,保证了线程安全,性能方面也得到了大幅提升
class Singleton8 {

    /*
    volatile
    可见性
    防止指令重排
     */
    private volatile static Singleton8 singleton;

    private Singleton8() {
    }

    public static Singleton8 getSingleton() {
        if (singleton == null) {
            // 可能多个线程走到这里
            synchronized (Singleton8.class) {
                // 可能一个线程释放了锁,其它线程拿到锁又进来了,
                // 所以还要进行非空判断
                if (singleton == null) {

                    /*
                    这里,防止指令重排
                    如果说我们不使用的话,
                    singleton = after Singleton();
                    ,这段代码其实是分为三步:
                    第一步:分配内存空间
                    第二步:初始化对象
                    第三步:将singleton对象指向分配的内存地址
                    volidate
                    保证上面三步得以顺序执行,
                    否则可能出现 第一步分配内存空间,第三步 引用指向内存地址,
                    此时,另外一个线程B走到第一个if (singleton == null)这里,此时确实不为null,
                    就可能导致线程B拿到的单例对象 是还没有被初始化,导致程序报错。
                     */
                    singleton = new Singleton8();
                }
            }
        }
        return singleton;
    }
}

2-9 使用ThreadLocal创建单例

这种只能保证,每个线程是单例
不同线程,是不一样的。
class Singleton9 {

    private static final ThreadLocal<Singleton9> singleton = new ThreadLocal<Singleton9>() {
        @Override
        protected Singleton9 initialValue() {
            return new Singleton9();
        }
    };

    public static Singleton9 getInstance() {

        return singleton.get();
    }

    private Singleton9() {
    }
}
posted @ 2021-12-11 22:47  姚狗蛋  阅读(33)  评论(0编辑  收藏  举报