Skip to content

Java设计模式-单例模式 (Singleton Pattern)

在一些程序设计中,希望对象只有一个实例,这时候就可以使用单例模式。

单例模式的实现,在语法上 用一个私有的构造方法来保护类不能在外部被 new 出来,然后提供一个静态方法返回唯一的实例即可。

应用场景,例如:系统配置,整个系统有一个配置对象即可,如果有配置修改,通知这个唯一的对象就好了,每次读取配置只需从这个唯一的对象中获取。

下面是一些常见的写法,以及优缺点:

实现方式一

package cn.devdoc.dp.creational.singleton;

/**
 * 最简单的单例模式,在多线程的情况下依然能保持单例。
 */
public class Singleton1 {

    private final static Singleton1 instance = new Singleton1();

    static {
        // 在这里初始化 instance 其实都一样,都是在类初始化即实例化instance。
    }

    private Singleton1() {

    }

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

这是最简单的单例模式,在多线程的情况下依然能保持单例。

这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,没有达到lazy loading的效果。

实现方式二

package cn.devdoc.dp.creational.singleton;

public class Singleton2 {

    private static Singleton2 instance;

    private Singleton2() {

    }

    public static Singleton2 getInstance() {
        if (instance == null) {
            // 在多线程的时候这里会出问题,导致的后果就是创建了多个实例
            // 为测试效果,假设这里需要5秒才能完成创建实例
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton2();
        }
        return instance;
    }
}

在调用getInstance方法时才实例化对象,但多线程环境下会创建出多个对象。

实现方式三

package cn.devdoc.dp.creational.singleton;

/**
 * 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
 */
public class Singleton3 {

    private static Singleton3 instance = null;

    private Singleton3() {

    }

    public static synchronized Singleton3 getInstance() {
        if (instance == null) {
            // 为测试效果,假设这里需要5秒才能完成创建实例
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton3();
        }
        return instance;
    }
}

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,除了第一次,其它情况都不需要同步。

实现方式四

package cn.devdoc.dp.creational.singleton;

/**
 * 双检锁,解决同步锁的性能问题,只有还没实例化的时候才会锁
 */
public class Singleton4 {

    private volatile static Singleton4 instance;

    private Singleton4() {

    }

    public static Singleton4 getInstance() {
        if (instance == null) {
            // 只有instance还没被实例化的时候才会到这里,但有可能多个线程都执行到这了。
            synchronized (Singleton4.class) {
                // 进入到if中的线程有多个,前面的线程可能已经实例化了instance,所以需要再次判断。
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

双检锁,解决同步锁的性能问题,只有还没实例化的时候才会锁。

实现方式五

package cn.devdoc.dp.creational.singleton;

/**
 * 基于静态内部类实现的单例模式
 */
public class Singleton5 {

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

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

通过内部类实现的,其它还有通过枚举实现的。

测试

package cn.devdoc.dp.creational.singleton;

import org.junit.Assert;
import org.junit.Test;

public class SingletonTest {

    @Test
    public void test0() {
        SingletonTest dp1 = new SingletonTest();
        SingletonTest dp2 = new SingletonTest();
        Assert.assertFalse(dp1.equals(dp2));
        Assert.assertFalse(dp1.hashCode() == dp2.hashCode());
    }

    @Test
    public void test1() {
        Singleton1 ins1 = Singleton1.getInstance();
        Singleton1 ins2 = Singleton1.getInstance();
        Assert.assertTrue(ins1.equals(ins2));
        Assert.assertTrue(ins1.hashCode() == ins2.hashCode());
        System.out.println(ins1);
        System.out.println(ins1.hashCode());
        // 模拟多线程的场景
        Runnable r = new Runnable() {
            @Override
            public void run() {
                Singleton1 single = Singleton1.getInstance();
                System.out.println(single.hashCode());
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
        }
    }

    @Test
    public void test2() {
        // 模拟多线程的场景,多运行几次会发现有不一样的hashCode
        Runnable r = new Runnable() {
            @Override
            public void run() {
                Singleton2 single = Singleton2.getInstance();
                System.out.println(single.hashCode());
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
        }
    }

    @Test
    public void test3() {
        // 模拟多线程的场景,无论怎么运行hashCode都是一样的
        Runnable r = new Runnable() {
            @Override
            public void run() {
                Singleton3 single = Singleton3.getInstance();
                System.out.println(single.hashCode());
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
        }
    }

    @Test
    public void test4() {
        // 模拟多线程的场景,无论怎么运行hashCode都是一样的
        Runnable r = new Runnable() {
            @Override
            public void run() {
                Singleton4 single = Singleton4.getInstance();
                System.out.println(single.hashCode());
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
        }
    }

    @Test
    public void test5() {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                Singleton5 single = Singleton5.getInstance();
                System.out.println(single.hashCode());
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
        }
    }
}

测试结果

test0测试出普通类new出来的2个对象 地址和hashCode都不一样

test1模拟线程测试hashCode是一样的

test2模拟线程测试会有hashCode不一样的情况这是多线程造成的

test3模拟线程测试无论怎么测试hashCode都一样

test4模拟线程测试无论怎么测试hashCode都一样

test5模拟线程测试无论怎么测试hashCode都一样