单例模式详细总结(优缺点、可用方案、演进与原理)-设计模式

本文详细介绍了单例模式,包括其优点、缺点、重点特性,如私有构造器、线程安全和延迟加载。文章讨论了饿汉式、懒汉式和枚举三种实现方案,推荐使用枚举单例模式,因其线程安全且避免了反序列化问题。同时,还探讨了懒汉式的演进,从基础的懒汉式到双重检查+volatile的优化版,以及静态内部类实现的懒汉式。
摘要由CSDN通过智能技术生成

 

目录

一、概述及可用方案

1、优点、缺点及重点

2、分类

3、可用方案

(1)饿汉式

(2)懒汉式

 (3)枚举

 

 

二、演进迭代与原理

1、基础的饿汉式

2、有线程安全问题的第一版懒汉式

3、加synchronized同步锁的懒汉式效率不高

4、双重检查+volatile 加synchronized同步锁的懒汉式效率优化版

5、用更简洁的 静态内部类实现懒汉式

6、枚举类型的单例模式


 

一、概述及可用方案

单例模式的意思是一个类只能产生一个实例,并提供全局访问。

使用场景一般有数据库连接池、线程池、配置项管理类等。

1、优点、缺点及重点

优点:

(1)在内存中只有一个实例,减少了内存开销。

(2)可以避免对资源的多重占用,比如写同一个日志文件。

缺点:

没有上层接口,扩展困难,只能直接修改该类的代码。比如Map m=new HashMap();这种语句单例模式中是不支持的,因为构造方法都是私有的或没有构造方法。

重点:

(1)私有构造器;

(2)线程安全;

(3)延迟加载(懒加载);

(4)序列化与反序列化安全;

(5)反射攻击。

前三个本篇都涉及到了,序列化和反射再单独开一篇文章学习。

2、分类

饿汉式、懒汉式、枚举

3、可用方案

优先推荐枚举单例模式,其次饿汉式,最后懒汉式。

(1)饿汉式

饿汉式缺点是程序部署加载的时候初始化会拉长程序加载时间、另外部分单例类实例化后可能用不到,导致资源浪费。

优点是提前加载好了,用到时访问不用再进行实例化。

构造方法设为private,防止外部访问。但是可以通过反射方式实现修改构造方法的访问权限。

/**
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就完成实例化
 * Class.forName("")
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

}

 

(2)懒汉式

懒汉式核心是要实现懒加载,什么时候用什么时候实例化。

为了保证线程安全,懒汉式包含静态内部类的实现方式和双重检查的实现方式。

静态内部类的实现方式:加载外部类时不会加载内部类,只有访问调用的时候才会加载,于是实现了懒加载。另外JVM保证了执行加载同一个内部类只加载一次,于是就保证了线程安全性,不会出现多个实例的情况。

/**
 * 静态内部类方式
 * JVM保证单例
 * 加载外部类时不会加载内部类,这样可以实现懒加载
 */
public class Mgr07 {

    private Mgr07() {
    }

    private static class Mgr07Holder {
        private final static Mgr07 INSTANCE = new Mgr07();
    }

    public static Mgr07 getInstance() {
        return Mgr07Holder.INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr07.getInstance().hashCode());
            }).start();
        }
    }


}

双重检查:volatile这里是必须要加的,防止语句重排导致实例在没有初始化时就被返回。

/**
 * 双重检查
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr06 {
    private static volatile Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

 (3)枚举

/**
 * 不仅可以解决线程同步,还可以防止反序列化。
 */
public enum Mgr08 {

    INSTANCE;

    public void m() {}

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr08.INSTANCE.hashCode());
            }).start();
        }
    }

}

 

 

二、演进迭代与原理

1、基础的饿汉式

基础的饿汉式可以满足大部分需求,它在程序初始化的时候产生实例。优缺点明显:

缺点:唯一不足在于提前产生实例,不管后面是否需要,这样做的类多了容易造成资源占用浪费。

优点:简单易实现,没有线程安全问题。提前加载,后期用到的时候无需重新产生实例的时间,提前准备,效率较高。

/**
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就完成实例化
 * Class.forName("")
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

}

2、有线程安全问题的第一版懒汉式

基于饿汉式的缺点,产生了懒加载的懒汉式。但第一版本的懒汉式单例模式存在线程安全问题。由于CPU分配时间的原因,可能导致一个线程执行到判断INSTANCE==null之后就被 挂起 了。下一个拿到时间片的线程又走了一次判断INSTANCE==null,也进入了if判断内部,由此new操作会重复执行。

public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {
    }

    public static Mgr03 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }


    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                System.out.println(Mgr03.getInstance().hashCode())
            ).start();
        }
    }
}

由此,需要进一步迭代,解决线程安全问题,搞出线程安全的懒汉式单例模式。

3、加synchronized同步锁的懒汉式效率不高

为了保证线程安全,我们进行了加synchronized同步锁的操作,由此同一时间点,只能有一个线程访问getInstance()方法,保证了线程安全,不会产生不同的实例对象。但这样的话,多线程的应用在这里会形成瓶颈,明明是并排的大马路,走着走着就成独木桥了,效率不高。


/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {
    }

    public static synchronized Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr04.getInstance().hashCode());
            }).start();
        }
    }
}

4、双重检查+volatile 加synchronized同步锁的懒汉式效率优化版

之前是不管你实例是否为空,调用getInstance()方法就会加锁。如果调整为实例为空的时候才进行加锁呢?

即调整加锁的位置缩小加锁范围会不会效率高一点呢?

public class Mgr05 {
    private static Mgr05 INSTANCE;

    private Mgr05() {
    }

    public static Mgr05 getInstance() {
        if (INSTANCE == null) {
            //妄图通过减小同步代码块的方式提高效率,然后不可行
            synchronized (Mgr05.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr05.getInstance().hashCode());
            }).start();
        }
    }
}

可这样又产生了新的线程安全问题。单重if不能保证线程安全,回到了懒汉加载初始版本的if判断线程安全问题。于是产生了双重检查式的懒汉单例模式,加锁后再判断一次,解决的是两个线程同时进了第一层if判断里面,但其中一个线程已经new操作过了的情况。但涉及到new对象,如果再分析new的详细步骤,则要在使用volatile关键字保证数据一致性,保证了线程安全。volatile这里是必须要加的,防止语句重排导致实例在没有初始化时就被返回。(这里的指令重排指的是JVM为了优化程序执行效率,在new这个操作的时候,做了指令重排。正常的顺序应该是1分配内存给这个对象、2初始化对象、3设置INSTANCE变量指向刚分配的内存地址。但如果2与3指令重排了,单线程没有问题,多线程可能会返回未经初始化的对象,造成程序问题。使用volatile禁止了指令重排)

那能不能拿掉第一层if,只保留下面代码中的第二层if呢?可以,不过这样就等价于上面的Mgr04中对 getInstance静态方法加锁的版本了(对静态方法加锁也是对Mgr.class加的锁,锁的也是当前的class对象)。

由此可以说保证懒加载的同时,进一步又优化了一下性能。不过缺点很明显,忒复杂了。

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr06 {
    private static volatile Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

5、用更简洁的 静态内部类实现懒汉式

进一步的,产生静态内部类的懒汉单例模式。利用的原理是一个类被加载时,其静态内部类不被加载,只有被访问调用到的时候才会被加载,由此实现懒加载。而如何保证静态内部类也只被加载一次呢?这个是由JVM实现保证的,由此就不会有线程不安全问题导致产生多个实例的问题了。(静态内部类只有被主动访问的时候才会进行初始化

/**
 * 静态内部类方式
 * JVM保证单例
 * 加载外部类时不会加载内部类,这样可以实现懒加载
 */
public class Mgr07 {

    private Mgr07() {
    }

    private static class Mgr07Holder {
        private final static Mgr07 INSTANCE = new Mgr07();
    }

    public static Mgr07 getInstance() {
        return Mgr07Holder.INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr07.getInstance().hashCode());
            }).start();
        }
    }


}

 

6、枚举类型的单例模式

《Effective Java》中的建议写法。

不仅可以解决线程安全问题,还能防止反序列化问题。之所以能够防止反序列化,是因为枚举类没有构造方法。而其他实现方法都是一般类,即使构造方法写成private,后面仍然可以通过反射的方式,修改构造方法的访问权限,实现构造出新的实例。

/**
 * 不仅可以解决线程同步,还可以防止反序列化。
 */
public enum Mgr08 {

    INSTANCE;

    public void m() {}

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr08.INSTANCE.hashCode());
            }).start();
        }
    }

}

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值