一天一个设计模式(二) - 单例模式(Singleton)

1,138 阅读5分钟

前言

单例模式 (Singleton) 是一种创建型模式,指某个类采用Singleton模式,则在这个类被创建后,只可能产生一个实例供外部访问,并且提供一个全局的访问点。

正文

(一). 优缺点

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

1. 优点

  • 提供了对唯一实例的受控访问。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  • 可以根据实际情况需要,在单例模式的基础上扩展做出双例模式,多例模式。

2. 缺点

  • 单例类的职责过重,里面的代码可能会过于复杂,在一定程度上违背了“单一职责原则”。
  • 如果实例化的对象长时间不被利用,会被系统认为是垃圾而被回收,这将导致对象状态的丢失。

(二). 具体实现

简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它们的实例。getInstance()的返回值是一个同一个对象的引用,并不是一个新的实例。单例模式 实现起来也很容易,以下给出六种实现方式:

1. 饿汉式

特点:线程安全,无法实现实例懒加载策略。

public class Singleton1 {
    private final static Singleton1 singleton1 = new  Singleton1();
    private Singleton1() {
    }

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

2. 懒汉式

特点:线程不安全,实现了实例懒加载策略。

public class Singleton2 {
    private final static Singleton2 singleton2;
    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        if (singleton2 == null)
            singleton2 = new Singleton2();
        return singleton2;
    }
}

3. 全局锁式

特点:线程安全,且实现了懒加载策略,但是线程同步时效率不高。

public class Singleton3 {
    private final static Singleton3 singleton3;
    private Singleton3() {
    }

    public synchronized static Singleton3 getInstance() {
        if (singleton3 == null)
            singleton3 = new Singleton3();
        return singleton3;
    }
}

4. 静态代码块式

特点:线程安全,类主动加载时才初始化实例,实现了懒加载策略,且线程安全。

public class Singleton4 {
    private final static Singleton4 singleton4;
    private Singleton4() {
    }
    static {
        singleton4 = new Singleton4();
    }

    public static Singleton4 getInstance() {
        return singleton4;
    }
}

5. 双重校验锁式

特点:线程安全,且实现了懒加载策略,同时保证了线程同步时的效率。但是volatile强制当前线程每次读操作进行时,保证所有其他的线程的写操作已完成。volatile使得JVM内部的编译器舍弃了编译时优化,对于性能有一定的影响。

public class Singleton5 {
    private static volatile Singleton5 singleton5;
    private Singleton5() {
    }

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

6. 静态内部类式【推荐】

特点:线程安全,不存在线程同步问题,且单例对象在程序第一次 getInstance()主动加载 SingletonHolder 和其 静态成员 INSTANCE,因而实现了懒加载策略。

public class Singleton6 {
    private Singleton6() {
    }

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

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

7. 枚举方式【作者推荐】

特点:线程安全,不存在线程同步问题,且单例对象在枚举类型 INSTANCE 第一次引用时通过枚举构造函数 初始化,因而实现了懒加载策略。

public class Singleton7 {
    private Singleton7() {
    }

    enum SingletonEnum {
        INSTANCE;

        private final Singleton7 singleton7;

        private SingletonEnum() {
            singleton7 = new Singleton7();
        }
    }

    public static Singleton7 getInstance() {
        return SingletonEnum.INSTANCE.singleton7;
    }

    public static void main(String[] args) {
        IntStream.rangeClosed(0, 100).forEach(i -> new Thread() {
            public void run() {
                out.println(Singleton7.getInstance());
            };
        }.start());
    }
}

这种方式是Effective Java作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊。不过,由于JDK 1.5中才加入enum特性,用这种方式写不免让人感觉生疏。

测试代码如下:

@FixMethodOrder
public class SingletonTester {
    protected final static int FROM = 0;
    protected final static int TO = 1000;

    protected static HashSet<Object> GLOBAL_SET = new HashSet<>();

    static {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                out.println();
                // count
                GLOBAL_SET.forEach((value) -> {
                    out.println("Global [" + value + "]");
                });
            }
        });
    }

    // testSingleton1
    @Test
    public void testSingleton1() throws Exception {
        final HashSet<Object> localSet = new HashSet<>();
        final CountDownLatch latch = new CountDownLatch(TO);
        IntStream.range(FROM, TO).forEach(i -> new Thread() {
            public void run() {
                Singleton1 singleton = Singleton1.getInstance();
                count(singleton);
            }

            protected void count(Singleton1 singleton) {
                localSet.add(singleton);
                out.println("Size of HashSet1 is: [" + localSet.size() + "]");
                // 计数减1,释放线程
                latch.countDown();
            };
        }.start());

        // 等待子线程执行结束
        latch.await();

        synchronized (localSet) {
            // count
            localSet.forEach((value) -> {
                out.println("[" + value + "]");
                out.println();
            });
            GLOBAL_SET.addAll(localSet);
        }
    }

    // testSingleton2
    // testSingleton3
    // testSingleton4
    // testSingleton5
    // testSingleton6
    // testSingleton7
}

测试结果截图如下,测试用例反映7种单例模式的方案都可以正常执行:

这里只演示其中一种单例方式,运行截图如下:

上图显示,通过 getInstance() 得到的实例全局唯一。对于其余六中方式,根据测试用例测试得到的结果一致,大家可以自行测试。

总结

本文总结了七种Java中实现单例模式的方法,其中使用双重校验锁静态内部类枚举类 的方式可以解决大部分问题。其中,极为推荐 静态内部类枚举类 这两种实现方式。


欢迎关注技术公众号: 零壹技术栈

零壹技术栈

本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。