Java单例模式

导读:本篇文章讲解 Java单例模式,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

单例模式属于设计模式三大分类中的第一类——创建型模式

这个模式在创建对象的同时,还致力于控制创建对象的数量,是的,只能创建一个实例

每个Java程序员都知道,Java中的对象都是使用new关键字来加载类并在堆内存中开辟空间创建对象,这是平时用到最多创建对象的方式。也知道每次new都会产生一个全新的对象。

那么问题来了,到底我们为什么要控制对象创建的个数?直接new一下多省事啊

既然这个模式存在并且大量使用,说明有些场景下,没它还真不行。那么什么场景下会没它不行呢?我举个栗子,比如我们平时使用的Windows上的回收站,是不是只有一个?要是有多个,会发生什么?我刚把回收站清空了,换到另一个回收站看垃圾还在,那这垃圾到底是在,还是不在?是不是很诡异了?另外比如博客上会有一个博客访问人数统计,这个东西要是不是单例的会有啥问题?今天统计了流量有100个,第二天用了一个新的计数器,又回到0了重新开始统计,那这个统计还有意义吗?

也就是说,有些场景下,不使用单例模式,会导致系统同一时刻出现多个状态缺乏同步,用户自然无法判断当前处于什么状态

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

单例有如下几个特点:

  • 在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在

  • 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例

  • 没有公开的set方法,外部类无法调用set方法创建该实例

  • 提供一个公开的get方法获取唯一的这个实例

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

  • 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销

  • 省去了new操作符,降低了系统内存的使用频率,减轻GC压力

  • 系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了

  • 避免了对资源的重复占用

饿汉式

当类一初始化,该类的对象就立刻会被实例化

public class Hungry {

    //构造器用private修饰,防止外部手动通过new创建
    private Hungry(){
    }

    //HUNGRY使用static修饰,然后调用new创建对象,我们知道static修饰的东西都属于类,而且在类加载阶段就已经被加载,并且只能被加载一次。就是类加载这种特性很好的保证了单例的特性,也天然防止了并发的问题。
    private static Hungry hugry = new Hungry();

    public static Hungry getHungry(){
        return hugry;
    }
}

问题:如果创建这个对象极其耗费时间和资源呢?这样必然会造成巨大的性能损耗。有的时候我只是想单纯的加载一下类,但并不想去用该对象,那这个时候这种模式就属于浪费内存了

当去访问一个类的静态属性的时候会触发该类初始化,这就导致,我明明只是想使用一下属性,并不想用

Hungry对象,但由于你访问了静态属性导致Hungry的初始化,从而导致HUNGRY被实例化,造成内存泄露。

懒汉式

需要用该对象才去创建对象

public class LazyMan {
    
    private LazyMan(){
        System.out.println(Thread.currentThread().getName());
    }

    private static LazyMan lazyMan;

    //问题 单线程没有问题,多线程有问题
    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }   
}

问题:多线程并发

public static void main(String[] args) {
        for (int i = 0; i < 10; i++){
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }

Java单例模式

解决方法:

双重检测锁模式 DCL懒汉式

private static LazyMan lazyMan;

public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                    /*
                    	1.申请内存空间
                    	2.初始化默认值
                    	3.执行构造器初始化
                    	4.将lazyMan指向创建的对象
                    */
                }
            }
        }
        return lazyMan;
    }

问题:不是原子性操作, 指令重排

些编译器会对代码做指令重排序,因为3和4本身相互并不存在依赖,指令重排序的存在可能会导致3和4顺序发生颠倒。

这会有什么问题?

首先在单线程下并不会有什么问题,为什么?因为指令重排序的前提就是不改变在单线程下的结果,无论先执行3还是4,最后返回的对象都是初始化好后的。

但是在多线程下呢?设想一种极端场景,现在假设A线程拿到锁进入到处,然后它完成了上面4步的1和2,因为现在指令重排序了,下面A线程会将instance指向创建的对象,也就是说,此时instance != null了!然后正当A要去执行构造器初始化对象时,巧得很,这时候B线程来到处,判断instance == null不成立了,直接返回,独留A线程在原地骂娘“尼玛,我还没初始化对象呢……”,因为返回了一个没有经过初始化的对象,后续操作自然会有问题。

正是因为这个原因,所以处volatile不可省略,主要原因就在于防止指令重排序,避免上述问题。那是不是这样就万无一失了呢?很遗憾,上述如此严密的控制,还是不能完全保证出问题。What?

那就是上述的做法有个前提,JDK必须是JDK5或更高版本,因为从JDK5才开始使用新的JSR-133内存模型规范,而在这个规范中才增强了volatile这个语义……

private volatile static LazyMan lazyMan;

public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();// 不是一个原子性操作 
                 }
            }
        }
        return lazyMan;
    }

就算不考虑JDK版本这个问题,这种方案的实现代码太过丑陋,本身看着就不是很爽,而且考虑的东西太多,稍有闪失就GG了。所以,这种方案虽然分析了这么多,但是其实没有实际意义,实际工作中强烈不建议使用

静态内部类

根据类加载机制,外部类的初始化并不会导致静态内部类的初始化。

这种方式是懒汉式的另一种方式,即做到了延迟加载,又避免了双重检测锁

public class Holder {
    //1.构造器私有化,外部不能new
    private Holder(){}

    //2.写一个静态内部类,直接返回Holder
    private static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
    
    //3.提供一个静态的公有方法,直接返回InnerClass.HOLDER
    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }
}

优缺点说明:

1) 这种方式采用了类装载的机制来保证初始化实例式只有一个线程。

2 ) 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

3) 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程时无法进入的。

4) 优点: 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。

5) 结论:推荐使用

使用枚举

//enum 本身也是一个class类
public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
public static void main(String[] args) {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        System.out.println(instance1);
        System.out.println(instance2);

    }

	/*
    输出结果
	INSTANCE
	INSTANCE
	*/

唯一遗憾的是,这个方案和饿汉式一样,没法延迟加载。枚举类加载自然就会初始化INSTANCE

如何选择

从安全性角度考虑,枚举显然是最安全的,保证绝对的单例,因为可以天然防止反射和反序列化的破解手段。而其它方案一定场合下全部可以被破解。

从延迟加载考虑,懒汉式、双重检测锁、静态内部类方案都可以实现,然而双重检测锁方案代码实现复杂,而且还有对JDK版本的要求,首先排除。懒汉式加锁性能较差,而静态内部类实现方法既能够延迟加载节约资源,另外也不需要加锁,性能较好,所以这方面考虑静态内部类方案最佳。

一般选用原则

单例对象占用资源少,不需要延时加载:枚举式好于饿汉式。

单例对象占用资源大,需要延时加载:静态内部类式好于懒汉式。

 


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/95931.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!