Java单例模式


Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。

单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。

单例模式有很多种写法,大部分写法都或多或少有一些不足。下面将分别对这几种写法进行介绍。

饿汉式(线程安全)

public class Singleton{
    private static final Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton newInstance(){
        return instance;
    }
} 

从代码中我们看到,类的构造函数定义为private的,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。

这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

懒汉式(线程安全)

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
} 

懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步

静态内部类(线程安全)

public class Singleton { 
    private Singleton(){
    }
    public static Singleton getSingleton(){  
        return Inner.instance;  
    }  
    private static class Inner {  
        private static final Singleton instance = new Singleton();  
    }  
} 

这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。

实现代码简洁。和双重检查单例对比,静态内部类单例实现代码真的是太简洁,又清晰明了。
延迟初始化。调用getSingleton才初始化Singleton对象。
线程安全。JVM在执行类的初始化阶段,会获得一个可以同步多个线程对同一个类的初始化的锁。

双重检验锁

public class Singleton {  
    private volatile static Singleton singleton;  //1:volatile修饰
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  //2:减少不要同步,优化性能
        synchronized (Singleton.class) {  // 3:同步,线程安全
        if (singleton == null) {  
            singleton = new Singleton();  //4:创建singleton 对象
        }  
        }  
    }  
    return singleton;  
    }  
}

延迟初始化。和懒汉模式一致,只有在初次调用静态方法getSingleton,才会初始化signleton实例。
性能优化。同步会造成性能下降,在同步前通过判读singleton是否初始化,减少不必要的同步开销。
线程安全。同步创建Singleton对象,同时注意到静态变量singleton使用volatile修饰。

为什么要使用volatile修饰?
虽然已经使用synchronized进行同步,但在第4步创建对象时,会有下面的伪代码:

memory=allocate(); //1:分配内存空间
ctorInstance();   //2:初始化对象
singleton=memory; //3:设置singleton指向刚排序的内存空间

当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以JVM是允许的。如果此时伪代码发生重排序,步骤变为1->3->2,线程A执行到第3步时,线程B调用getsingleton方法,在判断singleton==null时不为null,则返回singleton。但此时singleton并还没初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!

volatile如何实现防止指令重排序的呢?volatile是否还有别的作用的,有兴趣的同学可以去了解一下

上面提到的四种实现单例的方式都有共同的缺点:

  1. 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。

  2. 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。

枚举单例

public enum Singleton{
    instance;
    public void whateverMethod(){}    
}

高手之间的过招,必选择枚举单例模式。

下面附上测试枚举类解决序列化和反射问题的代码块,有兴趣的同学可以跟源码看一下为什么枚举类能解决此问题

// Copyright 2016-2101 Pica.
package com.pica.cloud.base.api.workstation.server.service;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @ClassName SingleTon
 * @Description TODO
 * @Author Chongwen.jiang
 * @Date 2020/5/10 10:46
 * @ModifyDate 2020/5/10 10:46
 * @Version 1.0
 */
public class SingleTon implements Serializable{
    private SingleTon(){}

    private volatile static SingleTon instance;

    private String content;

    public static SingleTon getInstance(){
        if(instance == null) {
            synchronized (SingleTon.class) {
                if(instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
        SingleTon s1 = SingleTon.getInstance();
        s1.setContent("内容答复");
        /*SingleTon s2 = SingleTon.getInstance();
        Constructor<SingleTon> constructor = SingleTon.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        SingleTon s3 = constructor.newInstance();
        System.out.println(s1 == s2);
        System.out.println(s1 == s3);*/

        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("SingleTon.obj"));
        os.writeObject(s1);
        os.flush();
        os.close();

        FileInputStream is = new FileInputStream("SingleTon.obj");
        ObjectInputStream oss = new ObjectInputStream(is);
        SingleTon s4 = (SingleTon)oss.readObject();
        oss.close();
        is.close();
        System.out.println(s4.getContent());
        System.out.println(s1 == s4);
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

// Copyright 2016-2101 Pica.
package com.pica.cloud.base.api.workstation.server.service;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @ClassName EnumSingleTon
 * @Description TODO
 * @Author Chongwen.jiang
 * @Date 2020/5/10 11:17
 * @ModifyDate 2020/5/10 11:17
 * @Version 1.0
 */
public enum EnumSingleTon implements Serializable {
    INSTANCE;
    EnumSingleTon(){}

    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
        EnumSingleTon s1 = EnumSingleTon.INSTANCE;
        s1.setContent("内容答复");
        EnumSingleTon s2 = EnumSingleTon.INSTANCE;
        Constructor<EnumSingleTon> constructor = EnumSingleTon.class.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);

        EnumSingleTon s3 = constructor.newInstance();
        System.out.println(s1 == s2);
        System.out.println(s1 == s3);

        /*ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("EnumSingleTon.obj"));
        os.writeObject(s1);
        os.flush();
        os.close();

        FileInputStream is = new FileInputStream("EnumSingleTon.obj");
        ObjectInputStream oss = new ObjectInputStream(is);
        EnumSingleTon s4 = (EnumSingleTon)oss.readObject();
        oss.close();
        is.close();
        System.out.println(s4.getContent());
        System.out.println(s1 == s4);*/
    }
}

枚举单例解决序列化、反射问题分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值