本文通过饿汉和懒汉两种模式分析单例模式,并分析每种单例的优缺点。

目录

1 饿汉-Hungry(可用)

2 懒汉-Lazy(不推荐)

3 线程安全-synchronized(可用)

4 双重检测-DoubleCheck(推荐)

5 内部类-InnerClass(推荐)

6 序列化-Serializable(可用)

7 容器单例-IOC(可用)

8 枚举-Enum(推荐)


1 饿汉-Hungry(可用)

饿汉单例模式在类加载的时候,就初始化,并且创建对象

优点:

1.没有加任何锁,执行效率比较高

2.用户体验上来说,比懒汉式更好

3.线程绝对安全,线程还没有访问的时候,就已经初始化了,不存在访问线程安全问题

缺点:

1.类加载的时候就初始化,不管你用不用,都占用内存资源

public class HungrySingleton {

    private static HungrySingleton instance = new HungrySingleton();
    //私有化构造函数
    private HungrySingleton(){

    }

    public static HungrySingleton getInstance(){
        return instance;
    }
}

还一种写法,在静态代码块中进行初始化,因为静态代码块也是在类加载的时候执行

public class HungryStaticSingleton {

    //私有化构造函数
    private HungryStaticSingleton() {
    }

    private static HungryStaticSingleton instance;
    //静态代码块
    static {
        instance = new HungryStaticSingleton();
    }

    public HungryStaticSingleton getInstance() {
        return instance;
    }
}

2 懒汉-Lazy(不推荐)

有线程调用getInstance方法的时候,开始进行初始化

优点:

1.开始不占用内存,只有调用了才初始化实例

缺点:

1.并发的时候,可能会出现多个实例。线程不安全,因为并发的时候,会出现创建多个实例的情况

public class LazySimpleSingleton {

    private static LazySimpleSingleton instance = null;
    //私有化构造函数
    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        if (instance == null) {
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

下面我们可以用两个线程来模拟下 ,创建一个ExecutorThread实现Runnable,并重写run方法

public class ExecutorThread implements Runnable {
    @Override
    public void run() {
        //获取实例
        LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
        //打印当前线程名和实例名
        System.out.println(Thread.currentThread().getName()+":"+instance);
    }
}

再创建一个Test类,创建两个线程并启动

public class LazySimpleSingletonTest {

    public static void main(String[] args) {

        new Thread(new ExecutorThread()).start();
        new Thread(new ExecutorThread()).start();
       
    }
}

 运行main方法,看控制台打印,从下图可以看出,有几率出现实例化两次的情况,


3 线程安全-synchronized(可用)

有线程调用getInstance方法的时候,开始进行初始化

优点:

1.开始不占用内存,只有调用了才初始化

2.线程安全,因为getInstance加上了锁,假如并发访问的话,拿到锁的线程,才能进入方法进行初始化,其他线程排队等待锁

缺点:

1.性能比较低,并发的时候,会出现其他线程等待的情况

2.每一次并发的时候,都会有锁的竞争情况

public class LazyThreadSingleton {

    private static LazyThreadSingleton lazySingleTon = null;
    //私有化构造函数
    private LazyThreadSingleton() {

    }

    public static synchronized LazyThreadSingleton getInstance() {
        if (lazySingleTon == null) {
            lazySingleTon = new LazyThreadSingleton();
        }
        return lazySingleTon;
    }
}

4 双重检测-DoubleCheck(推荐)

有线程调用getInstance方法的时候,开始进行初始化

优点:

1.开始不占用内存,只有调用了getInstance才初始化

2.第一次并发访问存在锁的竞争情况,后面任何一个一次并发都不会存在锁的竞争

缺点:

1.第一次并发的时候,有锁的竞争情况,存在线程等待

public class LazyDoubleCheckSingleton {
    //私有化构造函数
    private LazyDoubleCheckSingleton() {

    }

    private static LazyDoubleCheckSingleton instance = null;

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

5 内部类-InnerClass(推荐)

JVM加载LazyInnerClassSingleton的时候,不会去加载内部类LazyHolder,只有调用getInstance方法的时候,JVM才会去加载LazyHolder,巧妙的避开了线程安全的问题

优点:

1.开始不会占用内存

2.兼顾了synchronized的性能问题

3.巧妙的避免了线程安全的问题

public class LazyInnerClassSingleton {
    //私有化构造函数
    private LazyInnerClassSingleton() {

    }

    //保证这个方法不会被重写,重载
    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.lazy;
    }

    //内部类,默认不加载
    private static class LazyHolder {
        private static LazyInnerClassSingleton lazy = new LazyInnerClassSingleton();
    }
}

6 序列化-Serializable(可用)

两个要求:1.实现Seriablizable,2.增加readResolve方法

//实现Serializable
public class SeriableSingleton implements Serializable {
    //私有化构造函数
    private SeriableSingleton(){

    }

    private static SeriableSingleton instance = new SeriableSingleton();

    public static SeriableSingleton getInstance(){
        return instance;
    }
    //一定要加上这个方法,否则序列化会破坏单例
    public Object readResolve(){
        return instance;
    }
}

下面写个Test,对此对象进行序列化和反序列化

(序列化:就是把内存中的状态通过转换成字节码的形式从而转换成一个IO流,写入到其他地方,例如:网络IO,磁盘,内存中状态给永久保存下来了)

反序列化:将已经持久化的字节码内容,转换成IO流,通过IO流的读取,将内容转换成Java对象)

public class SeriableSingletonTest {

    public static void main(String[] args) {
        SeriableSingleton s1 = SeriableSingleton.getInstance();

        try {
            //序列化
            FileOutputStream fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s1);
            oos.flush();
            oos.close();
            //反序列化
            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            SeriableSingleton s2 = (SeriableSingleton) ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1==s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

查看控制台结果

7 容器单例-IOC(可用)

容器单例适用于创建非常多的实例,便于管理,就像Spring一样

public class ContainerSingleton {
    //私有化构造函数
    private ContainerSingleton() {

    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getInstance(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                try {
                    Object o = Class.forName(className).newInstance();
                    ioc.put(className, o);
                    return o;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return ioc.get(className);
        }

    }
}

我们可以看下Spring中的容器式单例的实现代码:

 

8 枚举-Enum(推荐)

public enum EnumSingleton {
    INSTANCE;

    EnumSingleton(){
        System.out.println("新建对象");
    }

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

写一个测试类,

public class EnumSingletonTest {
    public static void main(String[] args) {
        EnumSingleton instance1 = EnumSingleton.getInstance();
        EnumSingleton instance2 = EnumSingleton.getInstance();
        System.out.println(instance1==instance2);
    }
}

从结果可以看出,构造函数只执行了一次

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐