[关闭]
@babydragon 2016-04-19T12:57:16.000000Z 字数 5130 阅读 1129

Java单例模式实现

java


和其他面向对象语言类似,Java语言实现单例模式比较直接,通常的实现方式如下:

  1. public class Product
  2. {
  3. private Product() { }
  4. public static Product Instance() {
  5. return instance;
  6. }
  7. private static Product instance = new Product();
  8. public void DoSomething() {
  9. state++;
  10. System.out.println("I'm doing something for the " + state + " time");
  11. }
  12. private int state = 0;
  13. }
  14. public class Main
  15. {
  16. public static void main(String... args) {
  17. Product.Instance().DoSomething();
  18. Product.Instance().DoSomething();
  19. Product.Instance().DoSomething();
  20. }
  21. }

这个是一个最常规的单例创建方式,既:

  1. 将构造函数设置为私有,避免调用方通过构造函数创建出新的实例;
  2. 提供静态函数,返回预先创建好的对象。

针对Java语言的特性,上面示例中的单例创建方式,需要考虑以下一些问题。

作用域

根据Java虚拟机对于私有静态成员变量初始化的方式,示例中的instance成员变量在Product类初始化的时候已经完成初始化。由于Java类加载机制的特殊性,Product类的单例作用域事实上仅仅是加载了Product的类加载器。

也就是说,如果Product类通过不同的类加载器加载,每个类加载器会初始化一个实例,例如:

  1. public class Test {
  2. public static void main(String [] args) throws InterruptedException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
  3. invoke();
  4. invoke();
  5. }
  6. private static void invoke() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
  7. MyClassLoader c = new MyClassLoader();
  8. Class<?> productClass = c.loadClass("Product");
  9. Method instance = productClass.getMethod("Instance");
  10. Method doSomthing = productClass.getMethod("DoSomething");
  11. Object obj = instance.invoke(null);
  12. doSomthing.invoke(obj);
  13. }
  14. }

其中MyClassLoader是一个自定义的类加载器,上述代码会有以下输出:

  1. I'm doing something for the 1 time
  2. I'm doing something for the 1 time

序列化

虽然将单例对象序列化在一般应用场景中非常少见,但是如果单例类实现了Serializable接口,就需要考虑该类在反序列化后的场景。下面是一个简单的Java序列化/反序列化的示例:

  1. public class TestSerialization {
  2. public static void main(String [] args) throws IOException, ClassNotFoundException {
  3. Product p = Product.Instance();
  4. p.DoSomething();
  5. ByteArrayOutputStream bout = new ByteArrayOutputStream();
  6. ObjectOutputStream out = new ObjectOutputStream(bout);
  7. out.writeObject(p);
  8. ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
  9. ObjectInputStream in = new ObjectInputStream(bin);
  10. Product p1 = (Product)in.readObject();// 新对象,state == 1
  11. p.DoSomething(); // state == 2,但是不影响p1
  12. p1.DoSomething();
  13. }
  14. }

注意,这里Product和之前的基本相同,但是需要多实现Serializable接口。上述代码的输出为:

  1. I'm doing something for the 1 time
  2. I'm doing something for the 2 time
  3. I'm doing something for the 2 time

既反序列化之后的对象和原对象为不同的对象,但是保留了序列化当时的状态(这和“序列化”的概念相同)。因此,如果需要反序列化之后Product的实例仍然只有一个,就需要将反序列化后对象的状态和当前单例对象进行合并。

使用枚举实现单例

Java 1.5引入了枚举对象,枚举对象的每个枚举值都符合单例模式的描述,因此可以用枚举来实现Java的单例模式。

  1. public enum EnumProduct {
  2. INSTANCE;
  3. private int state = 0;
  4. public void DoSomething() {
  5. ++state;
  6. System.out.println("I did something for the " + state + " time");
  7. }
  8. }

使用枚举来实现单例有以下好处:

  1. 编写方便:无需按照“标准”单例方式,静态成员变量加私有构造函数。
  2. 序列化安全:由于枚举对象的特殊性,反序列化不会创建新的对象。

对于序列化,使用和前面序列化类似的例子:

  1. public class TestEnumSerialization {
  2. public static void main(String [] args) throws IOException, ClassNotFoundException {
  3. EnumProduct p = EnumProduct.INSTANCE;
  4. p.DoSomething();
  5. ByteArrayOutputStream bout = new ByteArrayOutputStream();
  6. ObjectOutputStream out = new ObjectOutputStream(bout);
  7. out.writeObject(p);
  8. ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
  9. ObjectInputStream in = new ObjectInputStream(bin);
  10. EnumProduct p1 = (EnumProduct)in.readObject();
  11. p.DoSomething();
  12. assert p == p1;
  13. }
  14. }

这里的p1对象虽然由ObjectInputStream反序列化而来,但是p和p1仍然是同一个对象,这和自己编写的单例对象有很大的区别。

另外,由于枚举的特殊性,使用枚举作为单例实现可能会遇到初始化问题和继承问题,当然这和使用场景有很大关系。

线程安全

在编写单例对象的时候,还有一个需要特别注意的地方,就是线程安全。由于JVM是一个多线程运行环境,单例对象的方法可能会并行的在多个线程中同时调用,因而产生多线程问题。

  1. public class ThreadsafeProduct
  2. {
  3. private ThreadsafeProduct() { }
  4. public static ThreadsafeProduct Instance() {
  5. return instance;
  6. }
  7. private static ThreadsafeProduct instance = new ThreadsafeProduct();
  8. public void DoSomething() {
  9. lock.lock();
  10. try {
  11. state++;
  12. System.out.println("I'm doing something for the " + state + " time");
  13. }
  14. finally {
  15. lock.unlock();
  16. }
  17. }
  18. private int state = 0;
  19. private Lock lock = new ReentrantLock();
  20. }

如上面示例那样,如果单例对象中存在状态,需要在访问时使用锁、原子变量等方式确保线程安全。

线程作用域下的单例

前面示例中的单例范围都是全局的(类加载器级别),但是Java语言针对线程还提供了ThreadLocal变量,使得我们能实现作用范围为线程的“单例模式”。如以下示例:

  1. public class ThreadProduct {
  2. private static ThreadLocal<ThreadProduct> threadLocal = new ThreadLocal<ThreadProduct>();
  3. public static ThreadProduct Instance() {
  4. ThreadProduct threadProject = threadLocal.get();
  5. if(threadProject == null) {
  6. threadProject = new ThreadProduct();
  7. threadLocal.set(threadProject);
  8. }
  9. return threadProject;
  10. }
  11. public void DoSomething() {
  12. state++;
  13. System.out.println("I'm doing something for the " + state + " time in " + Thread.currentThread().getName());
  14. }
  15. private int state = 0;
  16. }

示例中的ThreadProject类主要功能和前面的Product功能相同,但是静态方法在初始化对象的时候,既不是直接返回静态的对象,也不是直接实例化对象,而是尝试在ThreadLocal对象中获取。如果已经获取到了,说明针对当前线程的ThreadProject对象已经存在,直接返回;如果获取到的是null,说明针对当前线程的对象还不存在,则新创建一个对象(注意,这里没有线程安全)。我们通过以下代码来进行测试:

  1. public class TestThread {
  2. public static void main(String [] args) throws InterruptedException {
  3. Thread t1 = new Thread(new TestRunner());
  4. Thread t2 = new Thread(new TestRunner());
  5. ThreadProduct.Instance().DoSomething();
  6. t1.start();
  7. t2.start();
  8. t1.join();
  9. t2.join();
  10. }
  11. static class TestRunner implements Runnable {
  12. public void run() {
  13. IntStream.range(0, 3).forEach(i -> ThreadProduct.Instance().DoSomething());
  14. }
  15. }
  16. }

这里我们创建三个线程(包括主线程),每个线程都获取自己线程对应的ThreadProduct实例,然后执行DoSomething()方法。最终的输出为:

  1. I'm doing something for the 1 time in main
  2. I'm doing something for the 1 time in Thread-1
  3. I'm doing something for the 2 time in Thread-1
  4. I'm doing something for the 3 time in Thread-1
  5. I'm doing something for the 1 time in Thread-0
  6. I'm doing something for the 2 time in Thread-0
  7. I'm doing something for the 3 time in Thread-0

可以看见,每个线程都创建了自己线程作用域内的ThreadProduct对象。

总结

单例模式使用范围非常广泛,使用Java实现一个基本的单例对象也比较方便。除了基本的静态成员变量之外,使用枚举来实现单例也是一个不错的选择。当然,使用单例对象也有一些在特定业务场景下需要关注的点,如作用域、序列化、线程安全等。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注