Java 设计模式 – 单例模式 Singleton Pattern

在一些程序设计中,希望对象只有一个实例,这时候就可以使用单例模式。单例模式的实现,在语法上 用一个私有的构造方法来保护类不能在外部被 new 出来,然后提供一个静态方法返回唯一的实例即可。

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

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

实现方式一

package cn.devdoc.dp.creational.singleton;

/**
 * <p>
 * 最简单的单例模式,在多线程的情况下依然能保持单例。
 * </p>
 * 
 * @author CK
 *
 */
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;

/**
 * 
 * @author CK
 *
 */
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;

/**
 * <p>
 * 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
 * </p>
 * 
 * @author CK
 *
 */
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;

/**
 * <p>
 * 双检锁,解决同步锁的性能问题,只有还没实例化的时候才会锁
 * </p>
 * 
 * @author CK
 *
 */
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;

/**
 * <p>
 * 基于静态内部类实现的单例模式
 * </p>
 * 
 * @author CK
 *
 */
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 都一样
© 版权声明
THE END
喜欢就支持一下吧
点赞790 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容