纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

Java设计模式之单例模式 Java设计模式系列之深入浅出单例模式

三太子敖丙   2021-09-14 我要评论
想了解Java设计模式系列之深入浅出单例模式的相关内容吗三太子敖丙在本文为您仔细讲解Java设计模式之单例模式的相关知识和一些Code实例欢迎阅读和指正我们先划重点:java单例模式懒汉和饿汉,单例模式java,java单例设计模式下面大家一起来学习吧

前言

我不知道大家工作或者面试时候遇到过单例模式没面试的话我记得我当时在17年第一次实习的时候就遇到了单例模式面试官是我后来的leader当时就让我手写单例我记得我就写出了饿汉式懒汉式但是并没说出懒汉和饿汉的区别当时他给我一通解释我才知道了其中的奥秘

写这篇文章之前我刻意的在我手上的项目里面去找了找我发现单例在每个项目里面都有运用到而且我后面所说的几种实现还基本上都涉及了还挺有意思的

开篇我就给大家一个思考题:为什么不用静态方法而用单例模式?

问题的答案我会在最后公布大家可以带着问题看下去看看大家的思考是不是跟我一样的

大家肯定也能经常听到身边的同学说单例很简单自己也会但是真到自己的时候你能就一个知识点讲的很透彻并且能够发散思考引出更多的答案吗?或者能说出他每种模式更适合的场景么?这是值得深思的

首先给单例下一个定义:在当前进程中通过单例模式创建的类有且只有一个实例

单例有如下几个特点:

  • 在Java应用中单例模式能保证在一个JVM中该对象只有一个实例存在
  • 构造器必须是私有的外部类无法通过调用构造器方法创建该实例
  • 没有公开的set方法外部类无法调用set方法创建该实例
  • 提供一个公开的get方法获取唯一的这个实例

那单例模式有什么好处呢?

  • 某些类创建比较频繁对于一些大型的对象这是一笔很大的系统开销
  • 省去了new操作符降低了系统内存的使用频率减轻GC压力
  • 系统中某些类如spring里的controller控制着处理流程如果该类可以创建多个的话系统完全乱了
  • 避免了对资源的重复占用

好了单例模式的定义也清楚了好处也了解了先看一个饿汉式的写法

饿汉式

public class Singleton {
  // 创建一个实例对象
    private static Singleton instance = new Singleton();
    /**
     * 私有构造方法防止被实例化
     */
    private Singleton(){}
    /**
     * 静态get方法
     */
    public static Singleton getInstance(){
        return instance;
    }
}

之所以叫饿汉式大家可以理解为他饿他想提前把对象new出来这样别人哪怕是第一次获取这个类对象的时候直接就存在这个类了省去了创建类这一步的开销

等我介绍完懒汉之后对比一下大家就知道两者的区别以及各自适用在什么场景了

懒汉式

线程不安全的模式

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

懒汉式大家可以理解为他懒别人第一次调用的时候他发现自己的实例是空的然后去初始化了再赋值后面的调用就和饿汉没区别了

懒汉和饿汉的对比:大家可以发现两者的区别基本上就是第一次创作时候的开销问题以及线程安全问题(线程不安全模式的懒汉)

那有了这个对比那他们的场景好理解了在很多电商场景如果这个数据是经常访问的热点数据那我就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热)这样哪怕是第一个用户调用都不会存在创建开销而且调用频繁也不存在内存浪费了

而懒汉式呢我们可以用在不怎么热的地方比如那个数据你不确定很长一段时间是不是有人会调用那就用懒汉如果你使用了饿汉但是过了几个月还没人调用提前加载的类在内存中是有资源浪费的

线程安全问题

上面的懒汉我是故意没加锁的大家肯定都知道懒汉的线程安全问题的吧?

???忘了?那好吧暖男带你回忆一波吧

在运行过程中可能存在这么一种情况:多个线程去调用getInstance方法来获取Singleton的实例那么就有可能发生这样一种情况当第一个线程在执行if(instance==null)时此时instance是为null的进入语句

在还没有执行instance=new Singleton()时(此时instance是为null的)第二个线程也进入了if(instance==null)这个语句因为之前进入这个语句的线程中还没有执行instance=new Singleton() 所以它会执行instance = new Singleton()来实例化Singleton对象因为第二个线程也进入了if语句所以它会实例化Singleton对象

这样就导致了实例化了两个Singleton对象那怎么解决?

简单粗暴加锁就好了这是加锁之后的代码

public class Singleton {
    private static Singleton instance = null;
    /**
     * 私有构造方法防止被实例化
     */
    private Singleton(){}
    /**
     * 静态get方法
     */
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

这是一种典型的时间换空间的写法不管三七二十一每次创建实例时先锁起来再进行判断严重降低了系统的处理速度

有没有更好的处理方式呢?

有通过双检锁做两次判断代码如下:

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        //先检查实例是否存在如果不存在才进入下面的同步块
        if(instance == null){
            //同步块线程安全的创建实例
            synchronized (Singleton.class) {
                //再次检查实例是否存在如果不存在才真正的创建实例
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

将synchronized关键字加在了内部也就是说当调用的时候是不需要加锁的只有在instance为null并创建对象的时候才需要加锁性能有一定的提升

但是这样就没有问题了吗?

看下面的情况:在Java指令中创建对象和赋值操作是分开进行的也就是说instance = new Singleton();语句是分两步执行的

但是JVM并不保证这两个操作的先后顺序也就是说有可能JVM会为新的Singleton实例分配空间然后直接赋值给instance成员然后再去初始化这个Singleton实例

这样就可能出错了我们以A、B两个线程为例:

1、A、B线程同时进入了第一个if判断

2、A首先进入synchronized块由于instance为null所以它执行instance = new Singleton();

3、由于JVM内部的优化机制JVM先画出了一些分配给Singleton实例的空白内存并赋值给instance成员(注意此时JVM没有开始初始化这个实例)然后A离开了synchronized块

4、B进入synchronized块由于instance此时不是null因此它马上离开了synchronized块并将结果返回给调用该方法的程序

5、此时B线程打算使用Singleton实例却发现它没有被初始化于是错误发生了

加上volatile修饰Singleton再做一次优化:

public class Singleton {
    private volatile static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        //先检查实例是否存在如果不存在才进入下面的同步块
        if(instance == null){
            //同步块线程安全的创建实例
            synchronized (Singleton.class) {
                //再次检查实例是否存在如果不存在才真正的创建实例
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

**通过volatile修饰的变量不会被线程本地缓存所有线程对该对象的读写都会第一时间同步到主内存从而保证多个线程间该对象的准确性 **

volatile的作用

  • 防止指令重排序因为instance = new Singleton()不是原子操作
  • 保证内存可见

这个是比较完美的写法了这种方式能够安全的创建唯一的一个实例又不会对性能有太大的影响

但是由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化所以运行效率并不是很高还有更优的写法吗?

通过静态内部类

public class Singleton {  
  
    /* 私有构造方法防止被实例化 */  
    private Singleton() {  
    }  
  
    /* 此处使用一个内部类来维护单例 */  
    private static class SingletonFactory {  
        private static Singleton instance = new Singleton();  
    }  
  
    /* 获取实例 */  
    public static Singleton getInstance() {  
        return SingletonFactory.instance;  
    }  
  
    /* 如果该对象被用于序列化可以保证对象在序列化前后保持一致 */  
    public Object readResolve() {  
        return getInstance();  
    }  
}  

使用内部类来维护单例的实现JVM内部的机制能够保证当一个类被加载的时候这个类的加载过程是线程互斥的

这样当我们第一次调用getInstance的时候JVM能够帮我们保证instance只被创建一次并且会保证把赋值给instance的内存初始化完毕 这样我们就不用担心上面的问题

同时该方法也只会在第一次调用的时候使用互斥机制这样就解决了低性能问题这样我们暂时总结一个完美的单例模式

还有更完美的写法吗通过枚举:

public enum Singleton {
    /**
     * 定义一个枚举的元素它就代表了Singleton的一个实例
     */
    Instance;
}

使用枚举来实现单实例控制会更加简洁而且JVM从根本上提供保障绝对防止多次实例化是更简洁、高效、安全的实现单例的方式

最后这种也是我最青睐的一种(代码少)

总结

最后大家应该都知道单例模式的写法了也知道优劣势和使用场景了那开头的那个问题大家心里有答案了么?

什么?连问题都忘了?问题:为什么不用静态方法而用单例模式?

两者其实都能实现我们加载的最终目的但是他们一个是基于对象一个是面向对象的就像我们不面向对象也能解决问题一样面向对象的代码提供一个更好的编程思想

如果一个方法和他所在类的实例对象无关那么它就应该是静态的反之他就应该是非静态的如果我们确实应该使用非静态的方法但是在创建类时又确实只需要维护一份实例时就需要用单例模式了

我们的电商系统中就有很多类有很多配置和属性这些配置和属性是一定存在了又是公共的同时需要在整个生命周期中都存在所以只需要一份就行这个时候如果需要我再需要的时候new一个再给他分配值显然是浪费内存并且再赋值没什么意义

所以我们用单例模式或静态方法去维持一份这些值有且只有这一份值但此时这些配置和属性又是通过面向对象的编码方式得到的我们就应该使用单例模式或者不是面向对象的但他本身的属性应该是面对对象的我们使用静态方法虽然能同样解决问题但是最好的解决方案也应该是使用单例模式

资料参考:《java设计模式》、《为什么要用单例模式?》


相关文章

猜您喜欢

  • Mybatis 入门 一小时迅速入门Mybatis之初识篇

    想了解一小时迅速入门Mybatis之初识篇的相关内容吗grace.free在本文为您仔细讲解Mybatis 入门的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Mybatis,入门,Java,Mybatis下面大家一起来学习吧..
  • 从字符串里提取时间 java 怎样从字符串里面提取时间

    想了解java 怎样从字符串里面提取时间的相关内容吗cpown在本文为您仔细讲解从字符串里提取时间的相关知识和一些Code实例欢迎阅读和指正我们先划重点:java字符串,提取时间下面大家一起来学习吧..

网友评论

Copyright 2020 www.sopisoft.net 【绿软下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式