欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java单例模式的不同写法(懒汉式、饿汉式、双检锁、静态内部类、枚举)

程序员文章站 2024-03-14 20:16:41
...

简介

  在Java中单例(Singleton)模式是经常用到的一种设计模式;单例模式的主要作用是保证在Java程序中,类只有一个实例存在;它能保证以下几种好处
  1.可以避免实例对象的重复创建,实例对象没有重复创建,间接的就对资源的开销减少,资源的开销减少就减少时间的开销,内存空间的节省,有利于Java垃圾的回收
  2.可以保证一个类公有唯一的实例,可以避免多个实例导致的错误

单例模式的特点

  1.保证自己只有一个实例
  2.必须是自己创建自己的实例
  3.为外界提供唯一获取这个实例的唯一方法(私有化构造方法)

单例模式的写法

1.饿汉式

package com.jbit.mysingle;

/**
 * 饿汉式:最开始就创建好对象
 */
public class Single1 {
    //自己给自己new一个对象并且赋值
    private static final Single1 INSTANCE=new Single1();

    //将构造方法私有化,外界无法通过new的形式来创建这个类的对象
    private Single1(){

    }

    //给外界提供唯一的通过类创建对象的方法
    public static Single1 getInstance(){
        return INSTANCE;
    }

    public void print(){
        System.out.println("饿汉模式");
    }

    public static void main(String[] args) {
        Single1 s1=Single1.getInstance();
        Single1 s2=Single1.getInstance();
        System.out.println(s1==s2);
    }

}

好处:在加载类的时候就把实例创建好并且提供对外创建这个实例的方法,就算在多线程环境下也不会存在创建多个实例的情况,可以避免多线程同步的问题
缺点:就是不管这个类是否使用到它都会创建对象(Class.forName("")),浪费内存
使用场景:一般都是限定了这个类一开始,也就是初始化后就要使用到

2.懒汉

package com.jbit.mysingle;

/**
 * 懒汉:需要时才创建对象
 */
public class Single2 {
    //创建出一个类的对象,但是没有赋值
    private static Single2 INSTANCE;

    //私有化构造方法,自己提供特定获得对象的方法
    private Single2() {

    }

    //自己提供特定获得对象的方法
    //如果为null贼创建并且赋给自己类里面的对象
    //如果不为null就将值直接返回(这时值已经是不为空的了)
    public static Single2 getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Single2();
        }
        return INSTANCE;
    }
}


好处:需要的时候才去创建,假如单例已经创建,它就根据if条件去判断就不会创建了,就直接返回创建好的对象
缺点:这里的环境没有考虑到多线程的情况,假设现在多种线程同时去调用它的getInstance()方法,这时是INSTANCE是没有值的,就会创建出多个实例
使用场景:使用的次数少,创建这个单例消耗的资源多

懒汉模式的解决办法

1.给方法加synchronized关键字

package com.jbit.mysingle;

/**
 * 懒汉:需要时才创建对象
 */
public class Single3 {
    //创建出一个类的对象,但是没有赋值
    private static Single3 INSTANCE;

    //私有化构造方法,自己提供特定获得对象的方法
    private Single3() {

    }

    //自己提供特定获得对象的方法
    //如果为null贼创建并且赋给自己类里面的对象
    //如果不为null就将值直接返回(这时值已经是不为空的了)
    public synchronized static Single3 getInstance() {
            if (null == INSTANCE) {
                INSTANCE = new Single3();
            }
        return INSTANCE;
    }
}

2.给代码块加synchronized关键字

package com.jbit.mysingle;

/**
 * 懒汉:需要时才创建对象
 */
public class Single4 {
    //创建出一个类的对象,但是没有赋值
    private static Single4 INSTANCE;

    //私有化构造方法,自己提供特定获得对象的方法
    private Single4() {

    }

    //自己提供特定获得对象的方法
    //如果为null贼创建并且赋给自己类里面的对象
    //如果不为null就将值直接返回(这时值已经是不为空的了)
    public static Single4 getInstance() {
    	//想通过代码块的形式来加快效率的提升,但不太可行
    	//当第一个线程来的时候判断为null还没继续第二个线程来了,第二个线程申请上锁,锁好了执行代码new对象,然后执行方法释放锁;然后第一个线程继续运行,申请锁,new对象...
        synchronized(Single4.class){
            if (null == INSTANCE) {
                INSTANCE = new Single4();
            }
        }
        return INSTANCE;
    }
}

这两种虽然解决了在多线程环境下的多实例情况,但资源的浪费大大提高(本身synchronized修饰的东西就比一般的慢,而且当你外面有一大堆业务逻辑的时候,你的多个线程只能在外面等待,非常浪费资源)

所以就引出了现在推荐使用的双重校验锁(DCL(Double Check Lock))

双重校验锁(DCL(Double Check Lock))

package com.jbit.mysingle;

public class Single5 {
    private static Single5 INSTANCE;

    private Single5(){

    }

    public static Single5 getInstance(){
        //业务逻辑代码(如果不为空就直接返回对象执行)
        if(null == INSTANCE){   //Twice Checked
            //双重检查
            synchronized (Single5.class){
                if(null == INSTANCE){   //Double Checked
                    INSTANCE=new Single5();
                }
            }
        }
        return INSTANCE;
    }
}

这种方式很好的减少了多线程及性能的问题,大部分的代码都不会执行到synchronized这里,因为上面有一层if(null == INSTANCE)的判断;而里面这一层if(null == INSTANCE)为什么需要呢?假如没有的话,多个线程同时过了第一层的if(null == INSTANCE)的时候,多个线程进入到了synchronized代码块里面,就会分别创建这个对象

虽然双重校验锁实现了单例对象,线程并发问题,执行效率问题,但也不一定会万无一失,那是为什么呢?这里是因为CPU的乱序扫行,那么什么是乱序执行呢?

乱序执行

因为CPU的速度是内存的100倍的原因;当指令A发比例内存时,假设等待10秒,而指令B的速度只要3秒,那么它现在很有可能先执行B指令,B指令执行完了再执行A指令(谁先快先执行谁,不管顺序,底层是汇编实现的),怎么禁止?
答案是加vloatile

volatile

  volatile关键字规则凡是被它修饰的变量,对这个变量进行指令操作的时候都要加屏障,屏障就是上面一条指令,下面一条指令的时候中间给你加一道墙,这个墙就是内存屏障,内存屏障这里就不多说了;volatile的特性就很好解决了这个问题

所以修改后代码为这样

package com.jbit.mysingle;

public class Single5 {
    private static volatile Single5 INSTANCE;

    private Single5(){

    }

    public static Single5 getInstance(){
        //业务逻辑代码(如果不为空就直接返回对象执行)
        if(null == INSTANCE){   //Twice Checked
            //双重检查
            synchronized (Single5.class){
                if(null == INSTANCE){   //Double Checked
                    INSTANCE=new Single5();
                }
            }
        }
        return INSTANCE;
    }
}

静态内部类

package com.jbit.mysingle;

/**
 * 静态内部类
 */
public class Single6 {
    
    //通过内部类的实现去创建本类的对象
    private static class Single6Inside{
        public static Single6 instance=new Single6();
    }
    
    private Single6(){
        
    }
    
    //给外界提供创建对象的方法
    public static Single6 newInstance(){
        return Single6Inside.instance;
    }
    
}

静态内部类的实现其实和饿汉有点类型,都是通过类加载的时候就创建对象得到的;但不一样的是它是通过内部类去创建对象实例的,也就是说只要不使用这个内部类,就加载不到这个单例类,自然也无法创建

枚举

public enum  Single7 {
    INSTANCE;
}

获取方式及结果返回
Java单例模式的不同写法(懒汉式、饿汉式、双检锁、静态内部类、枚举)
现在很强烈使用枚举,至于为什么可以看:https://cloud.tencent.com/developer/article/1497592