关于单例模式,你是真的懂了吗?

引言:

说到单例模式(Singleton Pattern)相信大家都不陌生,它是Java中最基础的设计模式之一。
这种模式属于创建型模式,提供了一种创建对象的最佳方式。

简单点就是一个类只有一个实例,即不能被new一个出来了嘛,要访问它的时候就调用它给你提供的方法即可,不需要再重新实例化。

所以我们要做的事就很简单:设计一个类,只能有一个实例,并提供一个访问该实例的全局访问方法。

主要解决的问题:一个类被频繁创建和销毁

关键点:构造函数私有

 

实现:

今天我们按照逻辑来讲讲Java的8种单例模式的写法,每种的优劣都有分析

一:饿汉式

类加载在内存之后,就实例化了一个单例,JVM保证线程安全

这是一种比较简单实用的方法,比较推荐使用
缺点:不管你用不用,类装载的时候都会实例化,(话说回来,你不用的时候装载它干嘛。。。)

public class Mgr01 {
    //直接类装载的时候new一个
    private static final Mgr01 INSTANCE = new Mgr01();
    //这行代码是关键,保证构造方法是私有,别人不能new
    private Mgr01(){}
    //要就返回给你用
    public static Mgr01 getInstance(){return  INSTANCE;}

    public void m(){
        System.out.println("m");
    }
    //测试可用
    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();

        System.out.println(m1 == m2);
    }
}

二:饿汉式2

其实这种和第一种是一样的,看一看就好了

public class Mgr02 {
    private static final Mgr02 INSTANCE;
    static {
        INSTANCE = new Mgr02();
    }

    public static Mgr02 getInstance(){return  INSTANCE;}

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

    public static void main(String[] args) {
        Mgr02 m1 = Mgr02.getInstance();
        Mgr02 m2 = Mgr02.getInstance();

        System.out.println(m1 == m2);
    }
}

三:懒汉式1

既然饿汉式存在不管你用不用都会实例化的问题,那么自然就会有人想到懒汉式,

大概的思路就是我先不实例化,你要用了我再来实例化

存在的问题:不管你用不用都会实例化的小问题是解决了,却带来了更大的问题--线程不安全,具体可参考运行截图

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 void m(){
        System.out.println("m");
    }

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

运行截图:

四:懒汉式2

既然懒汉式1存在线程安全的问题,那么我们加个锁不就好了?

说的没错,很有道理,说做就做,我们给getInstance()方法加锁

但是同时也带来了新的问题--效率下降了。。

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 void m(){
        System.out.println("m");
    }

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

五:懒汉式3

既然给方法加锁效率低了,那么我们就只给实例化这个动作加锁呗,

想的没错,我们说做就做

事实证明,又变成线程不安全了。。

分析:见代码注释

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 void m(){
        System.out.println("m");
    }

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

}

六:懒汉式4(也叫双重判断)

既然懒汉式3还是存在问题,那么再加个判断是不是为空呗

说的有道理,我们来动手

这里改动就是在锁里面还加了一层判断,确实修复了上一个的bug

至此,懒汉式的单例模式算是圆满告了一段落

public class Mgr06 {
    private static Mgr06 INSTANCE;
    private Mgr06(){}

    public static Mgr06 getInstance(){
        if (INSTANCE == null){
            synchronized (Mgr05.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();
        }
    }
}

七:静态内部类(完美)

采用这种方式是JVM保证单例,也实现了懒加载,perfect

public class Mgr07 {
    private Mgr07(){}

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

    public static Mgr07 getInstance(){
        return Mgr07Holder.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(Mgr07.getInstance().hashCode());
            }).start();
        }
    }
}

八:枚举(完美中的完美)

其实上述7种方式,都是有问题的(友军,别打我,一般情况够用了,真的)

因为虽然你的构造方法是私有的,但是我们知道,Java里面是有反射的,我们可以通过反射获取该类的构造方法,这是避免不了的。

享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

网上扣的代码验证这个不安全

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Singleton s=Singleton.getInstance();
    Singleton sUsual=Singleton.getInstance();
    Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton sReflection=constructor.newInstance();
    System.out.println(s+"\n"+sUsual+"\n"+sReflection);
    System.out.println("正常情况下,实例化两个实例是否相同:"+(s==sUsual));
    System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s==sReflection));
}

“单元素的枚举类型已经成为实现Singleton的最佳方法”。

--霸气,这话可不是我说的,源自Java创始人之一的Joshua Bloch写的《effective java》这本书。(所以说没事多读书还是很重要的)

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();
        }
    }
}

what?这就写完了?

那妙在何处:首先枚举没有构造方法(当然也可以有,默认private,我们这里是没有),也避免了反射、反序列化的问题;

 

枚举类是JDK1.5才出现的,那之前的程序员面对反射攻击和序列化问题是怎么解决的呢?其实就是像Enum源码那样解决的,只是现在可以用enum可以使我们代码量变的极其简洁了。至此,相信同学们应该能明白了为什么Joshua Bloch说的“单元素的枚举类型已经成为实现Singleton的最佳方法”了吧,也算解决了我自己的困惑。

参考:

1、博客:为什么要用枚举实现单例模式(避免反射、序列化问题)

2、《Effective Java》(第2版):p14-15,p271-274
 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值