单例模式

单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并 提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,国家主席、公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接 池也都是单例形式。

饿汉式

/**单例模式之  饿汉式
 * @author gege
 * @Description
 * @date 2019/3/15 14:19
 */
public class EagerSingleton {

    //私有化构造方法
    private EagerSingleton(){}

    //全局提供一个单例实例
    private static final EagerSingleton EAGER_SINGLETON_INSTACE = new EagerSingleton();

    //对方提供一个访问接口
    public static EagerSingleton getInstance(){
        return EAGER_SINGLETON_INSTACE;
    }
}

我们先来回顾一下,类加载的顺序    

    1、先静态  后动态

    2、先属性、后方法

    3、先上后下

 

分析:

饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。 

优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。 

缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能占着茅 坑不拉屎。

Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例。

下面我们就来做测试,这种模式正常情况是单例且并发时也是安全的。我这里想通过反射来访问构造方法实例化对象

 @Test
    public void getInstance()  {
        Class<EagerSingleton> clazz = EagerSingleton.class;
        try {
            Constructor<EagerSingleton> constructor= clazz.getDeclaredConstructor(null);
            constructor.setAccessible(true);//强制访问  强吻

            //实例化对象
            EagerSingleton eagerSingleton= constructor.newInstance();
            EagerSingleton eagerSingleton2= constructor.newInstance();
            EagerSingleton eagerSingleton3 = EagerSingleton.getInstance();

            //输出
            System.out.println(eagerSingleton);
            System.out.println(eagerSingleton2);
            System.out.println(eagerSingleton3);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

 控制台打印:

eager.EagerSingleton@61e4705b
eager.EagerSingleton@50134894
eager.EagerSingleton@2957fcb0

结论出乎意料,通过反射强制访问私有构造 可以创建多个实例。此处改如何改进呢?敬请期待

其实我们只需在构造内添加一个判断

然后我们在执行刚刚的代码

 

 

我们用静态代码块来实现一下单例

/**
 * @author gege
 * @Description 静态代码块的机制 实现单例模式
 * @date 2019/3/15 14:54
 */
public class StaticEagerSingleton {
    //私有化构造方法
    private StaticEagerSingleton(){}
    //全局提供一个单例实例
    private static final StaticEagerSingleton EAGER_SINGLETON_INSTACE ;
    static {
        EAGER_SINGLETON_INSTACE = new StaticEagerSingleton();
    }
    //对方提供一个访问接口
    public static StaticEagerSingleton getInstance(){
        return EAGER_SINGLETON_INSTACE;
    }

以上两种饿汉式单例都很简单,也没什么区别。下面我们来看看懒汉式

懒汉式

简单的懒汉式代码实现

/**
 * @author gege  懒加载模式来实现基本的单例
 * @Description
 * @date 2019/3/15 15:04
 */
public class SimpleLazySingleton {
    private SimpleLazySingleton(){}
    private static SimpleLazySingleton simpleLazySingleton;
    public static SimpleLazySingleton getInstance(){
        if(simpleLazySingleton==null)
            simpleLazySingleton= new SimpleLazySingleton();
        return simpleLazySingleton;
    }
}

代码中把new 的动作推迟了,下面我们来测试

  @Test
    public void getInstance() {
        SimpleLazySingleton simpleLazySingleton = SimpleLazySingleton.getInstance();
        SimpleLazySingleton simpleLazySingleton1 = SimpleLazySingleton.getInstance();
        SimpleLazySingleton simpleLazySingleton2 = SimpleLazySingleton.getInstance();
        System.out.println(simpleLazySingleton);
        System.out.println(simpleLazySingleton1);
        System.out.println(simpleLazySingleton2);
    }

控制台

lazy.SimpleLazySingleton@61e4705b
lazy.SimpleLazySingleton@61e4705b
lazy.SimpleLazySingleton@61e4705b

看起来没问题

下面我们来尝试多线程并发来访问

首先我们写一个并发工具类(不是重点,不做详解)

/**
 * @author gege
 * @Description
 * @date 2019/3/15 15:08
 */
public class SimpleLazySingletonTest {


    @Test
    public void getInstance() {
        SimpleLazySingleton simpleLazySingleton = SimpleLazySingleton.getInstance();
        SimpleLazySingleton simpleLazySingleton1 = SimpleLazySingleton.getInstance();
        SimpleLazySingleton simpleLazySingleton2 = SimpleLazySingleton.getInstance();
        System.out.println(simpleLazySingleton);
        System.out.println(simpleLazySingleton1);
        System.out.println(simpleLazySingleton2);
    }

    //通过反射创建
    @Test
    public void getInstanceProxy()  {
        Class<SimpleLazySingleton> clazz = SimpleLazySingleton.class;
        try {
            Constructor<SimpleLazySingleton> constructor= clazz.getDeclaredConstructor(null);
            constructor.setAccessible(true);//强制访问  强吻

            //实例化对象
            SimpleLazySingleton simpleLazySingleton= constructor.newInstance();
            SimpleLazySingleton simpleLazySingleton2= constructor.newInstance();
            SimpleLazySingleton simpleLazySingleton3 = SimpleLazySingleton.getInstance();

            //输出
            System.out.println(simpleLazySingleton);
            System.out.println(simpleLazySingleton2);
            System.out.println(simpleLazySingleton3);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test//并发情况下
    public void concurrentInstance(){

    }
}
@Test//并发情况下
    public void concurrentInstance(){
        try {
            ConcurrentExecutor.execute(new ConcurrentExecutor.RunHandler() {
                public void handler() {
                    System.out.println(System.currentTimeMillis() + ": " + SimpleLazySingleton.getInstance());
                }
            },10,6);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

控制台

1552635063133: lazy.SimpleLazySingleton@7e541f7f
1552635063133: lazy.SimpleLazySingleton@36bb9ec9
1552635063133: lazy.SimpleLazySingleton@36bb9ec9
1552635063135: lazy.SimpleLazySingleton@36bb9ec9
1552635063135: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9

虽然我是执行好几次才出现不同的实例,但出现一次就足以说明此单例存在线程安全问题

如果你会用idea多线程debug,你可以这么写,

/**
 * @author gege
 * @Description
 * @date 2019/3/15 15:41
 */
public class ExectorThread implements  Runnable {
    public void run() {
        SimpleLazySingleton simpleLazySingleton = SimpleLazySingleton.getInstance();
        System.out.println(System.currentTimeMillis() + ": " +simpleLazySingleton);
    }
}
 @Test//并发情况下
    public void concurrentInstance1(){
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("end");
    }

然后用debug,手动控制3个线程的执行顺序也会出现多个实例的情况

Connected to the target VM, address: '127.0.0.1:65296', transport: 'socket'
1552636537928: lazy.SimpleLazySingleton@192fe644
1552636560953: lazy.SimpleLazySingleton@214b7af5
end
Disconnected from the target VM, address: '127.0.0.1:65296', transport: 'socket'

Process finished with exit code 0
经过测试 此单例出现线程安全的问题,在多线程并发情况下会出现多个实例,该如何优化呢?

我们自需在 getInstance 方法上加上 synchronized 关键字,使这个方法变成线程同步方法:

这时候,我们再来调试。当我们将其中一个线程执行并调用 getInstance()方法时,另一 个线程在调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻 塞。直到第一个线程执行完,第二个线程才恢复 RUNNING 状态继续调用 getInstance() 方法。

完美的展现了 synchronized 监视锁的运行状态,线程安全的问题便解决了。但是,用 synchronized 加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批 量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既 兼顾线程安全又提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式:

/**
 * @author gege  懒加载模式来实现基本的单例
 * @Description 该如何优化呢?
 * @date 2019/3/15 15:04
 */
public class SimpleLazySingleton2 {
    private SimpleLazySingleton2(){}
    private  static SimpleLazySingleton2 simpleLazySingleton;
    //在此处方法上加上同步锁
    public static  SimpleLazySingleton2 getInstance(){
        if(simpleLazySingleton==null){
            synchronized (SimpleLazySingleton2.class){
            if(simpleLazySingleton==null)simpleLazySingleton= new SimpleLazySingleton2();
            }
        }

        return simpleLazySingleton;
    }
}

当第一个线程调用 getInstance()方法时,第二个线程也可以调用 getInstance()。当第一 个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻 塞。此时,阻塞并不是基于整个 LazySimpleSingleton 类的阻塞,而是在 getInstance() 方法内部阻塞,只要逻辑不是太复杂,对于调用者而言感知不到。 但是,用到 synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。难 道就真的没有更好的方案吗?当然是有的。我们可以从类初始化角度来考虑,看下面的 代码,采用静态内部类的方式:

/**
 * @author gege  懒加载模式来实现基本的单例
 * @Description 静态内部类实现单例模式巧妙的运用了 静态内部类只有在该类被调用的时候才会加载,这个时候才开始实例化单例对象
 * @date 2019/3/15 15:04
 */
public class SimpleLazySingleton3 {
    //默认使用 SimpleLazySingleton3 的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private SimpleLazySingleton3(){
        if(InnerClass.SINGLETONINSTANCE!=null)
            throw new RuntimeException("单例已被破坏");
    }

    public static SimpleLazySingleton3 getInstance(){
        在返回结果以前,一定会先加载内部类
        return InnerClass.SINGLETONINSTANCE;
    }

    //默认不加载
    private static class InnerClass{
        //每一个关键字都不是多余的
        //static 是为了使单例的空间共享
        //保证这个方法不会被重写,重载
        private final static SimpleLazySingleton3 SINGLETONINSTANCE = new  SimpleLazySingleton3();
    }
}

上面我们已经提到过反射破坏单例及相应的解决办法

序列化破坏单例

下面我们来看一种反序列化破坏单例及解决办法

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当 于破坏了单例,来看一段代码:

    @Test
    public void getInstance() {
        SeriablesSingleton seriablesSingleton  =SeriablesSingleton.getInstance();
        System.out.println(seriablesSingleton);
        try {
            ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream("SeriablesSingleton.obj"));
            objectOutput.writeObject(seriablesSingleton);

            ObjectInput objectInput = new ObjectInputStream(new FileInputStream("SeriablesSingleton.obj"));
            SeriablesSingleton seriablesSingletonF = (SeriablesSingleton)objectInput.readObject();
            System.out.println(seriablesSingletonF);

        }catch (Exception e){

        }
    }

运行结果:

serializable.SeriablesSingleton@61e4705b
serializable.SeriablesSingleton@1e81f4dc

运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其 实很简单,只需要增加 readResolve()方法即可。来看优化代码:

/**
 * @author gege
 * @Description
 * @date 2019/3/18 13:50
 */
public class SeriablesSingleton implements Serializable {
    //序列化就是说把内存中的状态通过转换成字节码的形式
    //从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO)
    //内存中状态给永久保存下来了


    //反序列化
    //讲已经持久化的字节码内容,转换为 IO 流
    //通过 IO 流的读取,进而将读取的内容转换为 Java 对象
    //在转换过程中会重新创建对象 new
    public final static SeriablesSingleton INSTANCE = new SeriablesSingleton();
    private SeriablesSingleton(){}
    public static SeriablesSingleton getInstance(){
        return INSTANCE;
    }
    //针对反序列化破话单例的问题
    private Object readResolve(){
        return INSTANCE;
    }
}

看控制台打印:

serializable.SeriablesSingleton@61e4705b
serializable.SeriablesSingleton@61e4705b

虽然在单例类里面添加一个readResolve方法,但查看jdk源码的知,其实还是实例化了的,只是没把实例化的对象返回上来。那如果,创建对象的动作发生频率增大,就 意味着内存分配开销也就随之增大,难道真的就没办法从根本上解决问题吗?下面我们 来注册式单例也许能帮助到你。

注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标 识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。先来看枚举 式单例的写法,来看代码,创建 EnumSingleton 类:

/**
 * @author gege
 * @Description 注册式单例之枚举单例
 * @date 2019/3/18 14:41
 */
public enum EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

下面我们就来测试一下吧!

    @Test
    public void newInstance(){
        EnumSingleton enumSingleton  =EnumSingleton.INSTANCE;
        enumSingleton.setData(new Object());
        System.out.println(enumSingleton.getData());
        try {
            ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream("enumSingleton.obj"));
            objectOutput.writeObject(enumSingleton);

            ObjectInput objectInput = new ObjectInputStream(new FileInputStream("enumSingleton.obj"));
            EnumSingleton enumSingletonF = (EnumSingleton)objectInput.readObject();
            System.out.println(enumSingletonF.getData());

        }catch (Exception e){

        }
    }

控制台打印

java.lang.Object@61e4705b
java.lang.Object@61e4705b
true

竟然和我们的预想期是一样的,这是极力推荐的一种单例模式

 

注册式单例另一种写法,容器缓存的写法
/**
 * @author gege
 * @Description  注册式单例另一种写法,容器缓存的写法
 * @date 2019/3/18 15:41
 */
public class ContainerSingleton {
    private ContainerSingleton (){}
    public Map<Class,Object> map = new HashMap<Class,Object>();

    public Object get (Class clazz) {
        synchronized (map) {
            Object obj = map.get(clazz);
            if (obj == null) {
                try {
                    obj = clazz.newInstance();
                } catch (Exception e) {

                }
            }
            return obj;
        }
    }
}

单例模式小结 单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。 单例模式看起来非常简单,实现起来其实也非常简单。但是在面试中却是一个高频面试 题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值