深入单例模式
单例模式可以说只要是一个合格的开发都会写,但是如果要深究,小小的单例模式可以牵扯到喝多东西,比如 多线程是否安全,是否懒加载,性能等等。还有你知道几种单例模式的写法?如何防止反射或坏单例模式?今天 我们来探究单例模式。
1、饿汉式
public class Hungey {
private Hungey() {
}
private final static Hungey hungry = new Hungey();
public static Hungey getInstance() {
return hungry;
}
}
饿汉式是最简单的单例模式的写法,保证了线程的安全,在很长时间里,都是饿汉式来完成单例的,因为够简单,其实饿汉式有一点小问题,看下面代码:
public class Hungey {
private byte[] data1 = new byte[1024];
private byte[] data2 = new byte[1024];
private byte[] data3 = new byte[1024];
private byte[] data4 = new byte[1024];
private Hungey() {
}
private final static Hungey hungry = new Hungey();
public static Hungey getInstance() {
return hungry;
}
}
在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化了,并且放入内存了,如果长时间没有用到getInstance方法,不需要Hungry的对象,这不是一种浪费吗?我希望的是 只有用到了getInstance方法,才会去初始化单例类,才会去加载单例类中的数据。所以就有了第二种单例模式:懒汉式。
2、懒汉式
正常 懒汉式单例:
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"Start");
}
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 = 1; i <= 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
多一层检验可以避免问题,也就是DCL懒汉式
public class LazyMan {
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
DCL懒汉式的单例,保证了线程的安全性,又符合懒加载,只有在用到的时候,才会去初始化,调用效率也比较高,但是这种写法在极端情况下还是可能有一定的问题。因为
lazyMan = new LazyMan();
不是原子操作,至少经过三个步骤:
- 分配对象内存空间
- 执行构造方法初始化对象
- 设置instance执行刚分配的内存地址。
由于指令重排,导致A线程执行lazyMan=new LazyMan();的时候,可能会先执行了第三步(还没执行第二步),此时线程B又进来了发现lazyMan液晶部位空了,直接返回lazyMan,并且后面使用了返回的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错误,所以就有了下面的一种单例模式。
这种单例模式只是在上面DCL单例模式增加了一个volatile关键字来避免指令重排
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"Start");
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
3、静态内部类
还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接出示哈,不过是移到了静态内部类,十分巧妙。既保证了线程安全型,同时又满足了懒加载。
public class Holder {
private Holder() {
}
public static Holder getInstance() {
return InnerClass.holder;
}
private static class InnerClass{
private static final Holder holder = new Holder();
}
}
4、万恶的反射
万恶的反射登场了,反射是一个比较霸道的东西,无视private修饰的构造方法,可以直接在外面newInstance,破坏我们辛辛苦苦写的单例模式。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazyMan lazyMan = LazyMan.getInstance();
Constructor<LazyMan> declaredConstrutor = LazyMan.class.getDeclaredConstructor(null);
declaredConstrutor.setAccessible(true);
LazyMan lazyMan1 = declaredConstrutor.newInstance();
System.out.println(lazyMan.hashCode());
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan == lazyMan1);
}
我们分别打印出lazyMan,lazyMan1的hashcode,lazyMan1是否相等lazyMan,结果显而易见,不相等;那么,怎么解决这种问题呢?
public class LazyMan {
private LazyMan(){
synchronized (LazyMan.class) {
if (lazyMan != null) {
throw new RuntimeException("不要试图用反射破坏单例模式");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,如果正常调用getInstance方法,是不会出现这种事情的。
但是这种写法还是有问题:
上面我们是先正常的调用了getInstance方法,创建了LazyMan对象,所以第二次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败了。但是如果破坏者干脆不先调用getInstance方法,一上来就直接用反射创建对象,我们的判断就不生效了:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<LazyMan> declaredConstrutor = LazyMan.class.getDeclaredConstructor(null);
declaredConstrutor.setAccessible(true);
LazyMan lazyMan = declaredConstrutor.newInstance();
LazyMan lazyMan1 = declaredConstrutor.newInstance();
System.out.println(lazyMan.hashCode());
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan == lazyMan1);
}
那么如何防止这种反射破坏呢?
public class LazyMan {
private static boolean flag = false;
private LazyMan(){
synchronized (LazyMan.class) {
if (!flag) {
flag = true;
}else {
throw new RuntimeException("不要试图用反射破坏单例模式");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
在这里,我们定义一个boolean变量flag,初始值是false,私有构造函数里面做了一个判断,如果flag=false,就把flag改为true,但是如果flag等于true,就说明有问题了,因为正常调用时不会第二次跑到私有构造方法的,所以抛出异常。
看起来很美好,但是还是不能完全防止反射破坏单例模式,因为可以利用反射修改flag的值。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Constructor<LazyMan> declaredConstrutor = LazyMan.class.getDeclaredConstructor(null);
declaredConstrutor.setAccessible(true);
LazyMan lazyMan = declaredConstrutor.newInstance();
//获取lazyMan flag修改
Field field = LazyMan.class.getDeclaredField("flag");
field.setAccessible(true);
field.set(lazyMan, false);
LazyMan lazyMan1 = declaredConstrutor.newInstance();
System.out.println(lazyMan.hashCode());
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan == lazyMan1);
}
并没有一个很好地方案去避免反射破坏单例模式,所以论到我们的枚举登场了。
5、枚举
枚举类型是java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是一位它既是一种类(class)类型却又比类型多了些特殊的约束,但是这种约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance() {
return INSTANCE;
}
}
class Demo01 {
public static void main(String[] args) {
EnumSingleton singleton = EnumSingleton.INSTANCE;
EnumSingleton singleton2 = EnumSingleton.INSTANCE;
System.out.println(singleton == singleton2);
}
}
枚举是目前最推荐单例模式的写法,因为足够简单,不需要自己开发保证线程安全,同时又可以有效地防止反射破坏我们的单例模式,我们可以看下newInstance的源码:
重点是红框中圈出来的部分,如果枚举去newInstance就直接抛出异常了。
反编译查看下枚举的源码
javap -p EnumSingleton.class
Compiled from "EnumSingleton.java"
public final class singleton.EnumSingleton extends java.lang.Enum<singleton.EnumSingleton> {
public static final singleton.EnumSingleton INSTANCE;
private static final singleton.EnumSingleton[] $VALUES;
public static singleton.EnumSingleton[] values();
public static singleton.EnumSingleton valueOf(java.lang.String);
private singleton.EnumSingleton();
public singleton.EnumSingleton getInstance();
static {};
}
于是我们再次尝试使用反射空参构造生成枚举。
class Demo01 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton enumSingleton = constructor.newInstance();
}
}
出现了预期之外的错误Exception in thread "main" java.lang.NoSuchMethodException: singleton.EnumSingleton.<init>()
意为创建这个空参数构造器失败。
使用jad工具进行反编译:
jad -sjava EnumSingleton.class
点开这里的反编译java文件
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(singleton/EnumSingleton, name);
}
private EnumSingleton(String s, int i)
{
super(s, i);
}
public EnumSingleton getInstance()
{
return INSTANCE;
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
发现没有空参构造函数,只有一个(String,int)构造函数,我们改变反射再次尝试用反射创建枚举。
class Demo01 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingleton enumSingleton = constructor.newInstance();
}
}
确实无法破坏,出现了预期的错误Cannot reflectively create enum objects