单例模式


设计模式中最简单、面试的时候问,大部分人也只会说的那一个就是:单例模式。

定义

顾名思义,在整个系统中只能出现一个类的实例。比如全局配置信息。

实现方式

简单版本

package com.practice.test.gooddemo;

public class SingleTonDemo {
    private static SingleTonDemo singleTonDemo;

    private SingleTonDemo() {
    }

    public static SingleTonDemo getSingleTon() {
        if (singleTonDemo == null) {
            singleTonDemo = new SingleTonDemo();
        }
        return singleTonDemo;
    }
}
  • private 修饰构造函数,防止客户端随意创建实例。
  • 带有 static 关键字的属性在每一个类中都是唯一的(类级别的,在类初始化的时候就会加载)
  • 已经有了 private 修饰构造函数,那么必须有 static 修饰的函数去获取这个类的实例。

缺陷

如果在多线程环境下,可能会出现错误。当然只是可能,多线程环境下也可能只生成一个实例,但是我们需要的是代码的确定性。

# 示例代码:创建了 100 个线程同时访问。用 Set 去重并打印结果
public class SingleTonTest {

    public static void main(String[] args) throws InterruptedException {
        Set<String> set = new HashSet<String>();
        for (int i = 0; i < 100; i++) {
            new Thread() {
                public void run() {
                    SingleTonDemo s = SingleTonDemo.getSingleTon();
                    set.add(s.toString());
                }
            }.start();
        }

        for (String s : set) {
            System.out.println("打印:" + s);
        }
    }
}
//可能会打印一个、2个、3个

当一个线程去判断 singleTonDemo == null 成立时,还未生成实例,另一个线程到来判空,同样 singleTonDemo == null ,导致都会生成实例。所以违背了单例。

简单版本V1

既然多线程会出现问题,那么我们直接在获取实例的方法上面加上 synchronized 关键字,保证每个线程到来都会等待执行

# 直接在简单版本上面加了一个 synchronized 关键字
public class SingleTonDemo {
    private static SingleTonDemo singleTonDemo;

    private SingleTonDemo() {
    }

    public synchronized static SingleTonDemo getSingleTon() {
        if (singleTonDemo == null) {
            singleTonDemo = new SingleTonDemo();
        }
        return singleTonDemo;
    }
}

//  synchronized (SingleTonDemo.class) 这样的写法对于下面要讲的 双重检查更友好。效果和上面的写法一样
public class SingleTonDemo {
    private static SingleTonDemo singleTonDemo;

    private SingleTonDemo() {
    }

    public static SingleTonDemo getSingleTon() {
        synchronized (SingleTonDemo.class) {
            if (singleTonDemo == null) {
                singleTonDemo = new SingleTonDemo();
            }
        }
        return singleTonDemo;
    }
}

缺陷

这样是可以避免多线程造成对单例的破坏。但是创建一个实例后,其他操作都是读取而已。但是还是强制需要串行执行。太糟糕了。

简单版本V2==双重加锁

直接在整个 synchronized 块外面加上判空,当不为空就直接返回了。解决简单版本V1的问题:每次都需要等待获取。

这里不为空就直接返回了。

public class SingleTonDemo {
    private static SingleTonDemo singleTonDemo;

    private SingleTonDemo() {
    }

    public static SingleTonDemo getSingleTon() {
        if (singleTonDemo == null) { //基于上面的代码在整个 synchronized 块外面加上一个判空
            synchronized (SingleTonDemo.class) {
                if (singleTonDemo == null) {
                    singleTonDemo = new SingleTonDemo();
                }
            }
        }
        return singleTonDemo;
    }
}

这里两个判空的作用是:

  • 第一个:如果实例已经存在,不需要同步,就直接返回了(防止每个线程来获取实例,都要等待)
  • 第二个:被同步的线程,有一个创建了实例,另一个就不用了。(防止刚好两个线程同时判空,造成运行两次 new )

双重加锁 + volatile

深究双重加锁,也是有缺陷的。从 JVM 层面来讲:创建一个新的对象并非是原子性操作。对于下面这个语句,JVM 做了以下三件事:

singleton = new Singleton()

  1. 给 singleton 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
  3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错

public class SingleTonDemo {
    private volatile static SingleTonDemo singleTonDemo; //加上 volatile 关键字

    private SingleTonDemo() {
    }

    public static SingleTonDemo getSingleTon() {
        if (singleTonDemo == null) {
            synchronized (SingleTonDemo.class) {
                if (singleTonDemo == null) {
                    singleTonDemo = new SingleTonDemo();
                }
            }
        }
        return singleTonDemo;
    }
}

Volatile:

  • 这个变量不会在多个线程中存在复本,直接从内存中读取
  • 这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。

但是,这个事情仅在Java 1.5版后有用,1.5版之前用这个变量也有问题,因为老版本的Java的内存模型是有缺陷的。

简化版本

public class SingleTonDemo {
    private SingleTonDemo(){}

    private static class SingletonHolder{
        private static final SingleTonDemo INSTANCE = new SingleTonDemo();
    }

    public static final SingleTonDemo getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 一个类的静态属性只会在第一次加载类时初始化,这是 JVM 帮我们保证的,所以无需担心并发问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为 JVM 会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以 singleton 仍然是单例的。

  • 上面这种方式,仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

总结

到这里,单例模式算是差不多了。以上的形式保证了如下几点:

1.Singleton最多只有一个实例,在不考虑反射强行突破访问限制的情况下。

2.保证了并发访问的情况下,不会发生由于并发而产生多个实例。

3.保证了并发访问的情况下,不会由于初始化动作未完全完成而造成使用了尚未正确初始化的实例。

暴力反射下:上面的都不存在单例。因为可以通过反射暴力调用私有构造函数。而私有构造函数是实现单例的一个强有力保证

下面的反射案例,是通过 Constructor 去调用单例类的私有构造函数。(相当于 new XXX();所以每次调用都会生成不同的对象)

// 反射获取的 Class 是上面简化版本的类
public class SingleTonTest {

    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        for (int i = 0; i < 10; i++) {
            try {
                Class c = Class.forName("com.practice.test.gooddemo.SingleTonDemo");
                Constructor constructor = c.getDeclaredConstructor();
                constructor.setAccessible(true);
                Object obj = constructor.newInstance();
                set.add(obj.toString());
            } catch (Exception e) {
            }
        }

        for (String s : set) {
            System.out.println("打印:" + s);
        }
    }
}

tip:优雅版本 + 防反射

最新版的《Effctive Java》推荐的方式:

默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。

//定义一个单元素的枚举类
public enum InstanceDemo {
   INSTANCE;
}

//暴力反射调用枚举类
public class SingleTonTest {
    public static void main(String[] args) throws Exception {
        InstanceDemo s = InstanceDemo.INSTANCE;
        Constructor<InstanceDemo> constructor = InstanceDemo.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        InstanceDemo obj = constructor.newInstance();
        System.out.println(obj == s);
    }
}


//运行结果:没有这个方法(空的构造函数)
Exception in thread "main" java.lang.NoSuchMethodException: com.practice.test.gooddemo.InstanceDemo.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.practice.test.gooddemo.SingleTonTest.main(SingleTonTest.java:13)
//通过反射调用方法,只能指定调用,比如获取构造函数,也只能完全匹配方法、参数。

通过 Enum 类,查看到底是什么构造函数,在IDEA中找到 Enum 类,然后 ctrl+f12 :展示此类所有属性方法

//唯一一个构造函数,所以才有我们上面反射报错
protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}
public class SingleTonTest {

    public static void main(String[] args) throws Exception {
        InstanceDemo s = InstanceDemo.INSTANCE;
        Constructor<InstanceDemo> constructor = InstanceDemo.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        //上面会获取到构造方法,但是下面实例化会报错
        InstanceDemo obj = constructor.newInstance();
        System.out.println(obj == s);
    }
}


//结果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.practice.test.gooddemo.SingleTonTest.main(SingleTonTest.java:16)
    
//debug 进去就一目了然了:是放射类型就抛异常
     if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

因此枚举是防反射的。对于序列化与反序列化,也可以简单测试:

public class SingleTonTest {
	//序列化-反序列化也没问题的
    public static void main(String[] args) {
        InstanceDemo s = InstanceDemo.INSTANCE;
        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);
        System.out.println(s == deserialize); //true
    }
}

so~,枚举是实现单例的首选。

参考

装饰器模式详解-与IO不解的情缘

深入浅出单实例SINGLETON设计模式

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值