Chinaunix首页 | 论坛 | 博客
  • 博客访问: 55955
  • 博文数量: 34
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 378
  • 用 户 组: 普通用户
  • 注册时间: 2017-01-17 11:19
  • 认证徽章:
个人简介

人的一生犹如负重致远,不可急躁。 以不自由为常事,则不觉不足。 心生欲望时,应回顾贫困之日。 心怀宽恕,视怒如敌,则能无视长久。 只知胜而不知敗,必害其身。 责人不如责己,不及胜于过之。

文章分类

全部博文(34)

文章存档

2018年(2)

2017年(32)

我的朋友

分类: Java

2017-03-02 19:37:23

Java单例模式

 

1  单例模式

单例模式是在项目开发中最为常见的设计模式之一,而单例模式在计算机系统中有很多应用,例如线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。

单例的特性:

1、  在任何情况下,单例类永远只有一个实例存在。

2、  单例需要有能力为整个系统提供这一唯一实例。

2  单例模式会存在的问题

单例模式有很多种实现方式,但是在不同环境中可能遇到可能隐藏着一些问题:

多线程并发下如何保证单例模式的线程安全性呢?如何保证序列化后的单例对象在反序列化后任然是单例的呢?

3  饿汉式单例

         饿汉式单例是我最推荐的,简单易懂,而且在JVM层实现了线程安全。

3.1  静态成元初始化特性

         饿汉式单例是指在方法调用前,实例就已经创建好了。

实现代码:

点击(此处)折叠或打开

  1. public class HungrySingleton {
  2.     private static HungrySingleton instance = new HungrySingleton();

  3.     private HungrySingleton() {

  4.     }

  5.     public static HungrySingleton getInstance() {
  6.         return instance;
  7.     }
  8. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.         System.out.println(HungrySingleton.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.        Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


运行结果:

点击(此处)折叠或打开

  1. 857928231
  2. 857928231
  3. 857928231
  4. 857928231
  5. 857928231
  6. 857928231
  7. 857928231
  8. 857928231
  9. 857928231

3.2  static代码块实现单例

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性的实现单例设计模式。

实现代码:

点击(此处)折叠或打开

  1. public class StaticCodeSingleton {
  2.     private static StaticCodeSingleton instance = null;

  3.     private StaticCodeSingleton() {

  4.     }

  5.     static {
  6.         instance = new StaticCodeSingleton();
  7.     }

  8.     public static StaticCodeSingleton getInstance() {
  9.         return instance;
  10.     }
  11. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.         System.out.println(StaticCodeSingleton.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


测试结果:

点击(此处)折叠或打开

  1. 1501649966
  2. 1501649966
  3. 1501649966
  4. 1501649966
  5. 1501649966
  6. 1501649966
  7. 1501649966
  8. 1501649966
  9. 1501649966


3.3  静态内部类

静态内类在多线程并发下的线程安全问题,其实使用其他方式也可以达到同样的效果。

实现代码:

点击(此处)折叠或打开

  1. public class InnerClassSingleton {
  2.     private InnerClassSingleton() {

  3.     }

  4.     private static class Handler {
  5.         private static InnerClassSingleton instance = new InnerClassSingleton();
  6.     }

  7.     public static InnerClassSingleton getInstance() {
  8.         return Handler.instance;
  9.     }
  10. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.         System.out.println(InnerClassSingleton.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


测试结果:

点击(此处)折叠或打开

  1. 325346962
  2. 325346962
  3. 325346962
  4. 325346962
  5. 325346962
  6. 325346962
  7. 325346962
  8. 325346962
  9. 325346962


4  懒汉式单例

懒汉式单例是指在方法调用获取实例时才创建实例,因为相对饿汉式显得“不急迫”,所以被叫做“懒汉模式”。

实现代码:

点击(此处)折叠或打开

  1. public class LazySingleton {
  2.     private static LazySingleton instance = null;

  3.     private LazySingleton() {

  4.     }

  5.     public static LazySingleton getInstance() {
  6.         if(instance == null){
  7.             instance = new LazySingleton();
  8.         }
  9.         return instance;
  10.     }
  11. }


    注意:懒汉式的单例存在多线程并发问题无法保证实例唯一。
    下面我们对上面的代码做点小的修改,在多线程线测试一下。
修改后的代码:

点击(此处)折叠或打开

  1. public class LazySingleton {
  2.     private static LazySingleton instance = null;

  3.     private LazySingleton() {

  4.     }

  5.     public static LazySingleton getInstance() {
  6.         try {
  7.             if(instance == null){
  8.                 Thread.sleep(50);
  9.                 instance = new LazySingleton();
  10.             }
  11.         } catch (InterruptedException e) {
  12.             e.printStackTrace();
  13.         }
  14.         return instance;
  15.     }
  16. }


    加Thread.sleep(50)目的是假设在创建实例前有一些准备性的耗时工作要处理,多线程调用。
测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.           System.out.println(LazySingleton.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


执行结果:

点击(此处)折叠或打开

  1. 1792662872
  2. 2016090147
  3. 2078766677
  4. 72720249
  5. 339200137
  6. 1792662872
  7. 112662297
  8. 1744251680
  9. 740994369


4.1  线程安全懒汉式单例

要保证线程安全,我们就得需要使用同步锁机制。加同步锁有好多种方式,下面一步步的解决存在线程安全问题的懒汉式单例。

4.1.1  方法声明synchronized关键字

出现非线程安全问题,是由于多个线程可以同时进入getInstance()方法,那么只需要对该方法进行synchronized的锁同步。

实现代码:

点击(此处)折叠或打开

  1. public class LazySingleton {
  2.     private static LazySingleton instance = null;

  3.     private LazySingleton() {

  4.     }

  5.     public synchronized static LazySingleton getInstance() {
  6.         try {
  7.             if(instance == null){
  8.                 Thread.sleep(50);
  9.                 instance = new LazySingleton();
  10.             }
  11.         } catch (InterruptedException e) {
  12.             e.printStackTrace();
  13.         }
  14.         return instance;
  15.     }
  16. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.           System.out.println(LazySingleton.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


测试结果:

点击(此处)折叠或打开

  1. 858368339
  2. 858368339
  3. 858368339
  4. 858368339
  5. 858368339
  6. 858368339
  7. 858368339
  8. 858368339
  9. 858368339


注意:从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低。

4.1.2  同步代码块

对某些重要的代码进行单独的同步,而不是全部进行同步,可以极大的提高执行效率。

实现代码:

点击(此处)折叠或打开

  1. public class LazySingleton {
  2.     private static LazySingleton instance = null;

  3.     private LazySingleton() {

  4.     }

  5.     public static LazySingleton getInstance() {
  6.         try {
  7.             synchronized (LazySingleton.class) {
  8.                 if(instance == null){
  9.                     Thread.sleep(50);
  10.                     instance = new LazySingleton();
  11.                 }
  12.             }
  13.         } catch (InterruptedException e) {
  14.             e.printStackTrace();
  15.         }
  16.         return instance;
  17.     }
  18. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.           System.out.println(LazySingleton.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


测试结果:

点击(此处)折叠或打开

  1. 397454999
  2. 397454999
  3. 397454999
  4. 397454999
  5. 397454999
  6. 397454999
  7. 397454999
  8. 397454999
  9. 397454999


注意:这里的实现能够保证多线程并发下的线程安全性,但这样的实现和将全部的代码都被锁上效率是一样很低下。

4.1.3  对重要的代码来进行单独的同步(可能非线程安全)

         针对某些重要的代码进行单独的同步,而不是全部进行同步,可以极大的提高执行效率。

实现代码:

点击(此处)折叠或打开

  1. public class LazySingleton {
  2.     private static LazySingleton instance = null;

  3.     private LazySingleton() {

  4.     }

  5.     public static LazySingleton getInstance() {
  6.         try {
  7.             if(instance == null){
  8.                 Thread.sleep(50);
  9.                 synchronized (LazySingleton.class) {
  10.                     instance = new LazySingleton();
  11.                 }
  12.             }
  13.         } catch (InterruptedException e) {
  14.             e.printStackTrace();
  15.         }
  16.         return instance;
  17.     }
  18. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.           System.out.println(LazySingleton.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


测试结果:

点击(此处)折叠或打开

  1. 325346962
  2. 1877810053
  3. 858368339
  4. 49073278
  5. 552717651
  6. 1817950348
  7. 1213219390
  8. 1501649966
  9. 1850765544


4.1.4  双检查机制(Double Check Locking

         为了达到线程安全,又能提高代码执行效率,我们这里可以采用DCL的双检查锁机制来完成。

实现代码:

点击(此处)折叠或打开

  1. public class LazySingleton {
  2.     volatile private static LazySingleton instance = null;

  3.     private LazySingleton() {

  4.     }

  5.     public static LazySingleton getInstance() {
  6.         try {
  7.             if(instance == null){
  8.                 Thread.sleep(50);
  9.                 synchronized (LazySingleton.class) {
  10.                     if (instance == null) {
  11.                         instance = new LazySingleton();
  12.                     }
  13.                 }
  14.             }
  15.         } catch (InterruptedException e) {
  16.             e.printStackTrace();
  17.         }
  18.         return instance;
  19.     }
  20. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.           System.out.println(LazySingleton.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }

测试结果:

点击(此处)折叠或打开

  1. 1137452872
  2. 1137452872
  3. 1137452872
  4. 1137452872
  5. 1137452872
  6. 1137452872
  7. 1137452872
  8. 1137452872
  9. 1137452872



从运行结果来看,该中方法保证了多线程并发下的线程安全性。这里在声明变量时使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化。集合其二者,这种实现方式既保证了其高效性,也保证了其线程安全性。

双重检查自身会存在比较隐蔽的问题,查了Peter HaggarDeveloperWorks上的一篇文章,对二次检查的解释非常的详细:“双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。

双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入,这也是这些习语失败的一个主要原因”。

 

5  序列化与反序列化的单例模式实现

5.1 未实现序列化与反序列化的单例模式实现

         静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

实现代码:

点击(此处)折叠或打开

  1. public class SerialSingleton implements Serializable {

  2.     private static final long serialVersionUID = 1L;

  3.     private SerialSingleton() {

  4.     }

  5.     private static class Handler {
  6.         private static SerialSingleton instance = new SerialSingleton();
  7.     }

  8.     public static SerialSingleton getInstance() {
  9.         return Handler.instance;
  10.     }
  11. }


测试代码:

点击(此处)折叠或打开

  1. import java.io.*;

  2. public class PersistenceSerialSingleton {
  3.     public static void main(String[] args) throws FileNotFoundException {
  4.         SerialSingleton singleton = SerialSingleton.getInstance();

  5.         File file = new File("SerialSingleton.dat");
  6.         try {
  7.             FileOutputStream fos = new FileOutputStream(file);
  8.             ObjectOutputStream oos = new ObjectOutputStream(fos);
  9.             oos.writeObject(singleton);
  10.             fos.close();
  11.             oos.close();
  12.             System.out.println(singleton.hashCode());
  13.         } catch (FileNotFoundException e) {
  14.             e.printStackTrace();
  15.         } catch (IOException e) {
  16.             e.printStackTrace();
  17.         }

  18.         try {
  19.             FileInputStream fis = new FileInputStream(file);
  20.             ObjectInputStream ois = new ObjectInputStream(fis);
  21.             SerialSingleton PersistenceSingleton = (SerialSingleton)ois.readObject();
  22.             fis.close();
  23.             ois.close();
  24.             System.out.println(PersistenceSingleton.hashCode());
  25.         } catch (FileNotFoundException e) {
  26.             e.printStackTrace();
  27.         } catch (IOException e) {
  28.             e.printStackTrace();
  29.         } catch (ClassNotFoundException e) {
  30.             e.printStackTrace();
  31.         }
  32.     }
  33. }


测试结果:

点击(此处)折叠或打开

  1. 325040804
  2. 1173230247


从结果中我们发现,序列号对象的hashCode和反序列化后得到的对象的hashCode值不一样,说明反序列化后返回的对象是重新实例化的,单例被破坏了。

5.2  解决反序列化后返回的对象重新实例化

解决办法就是在反序列化的过程中使用readResolve()方法。

实现代码:

点击(此处)折叠或打开

  1. import java.io.Serializable;
  2. import java.io.ObjectStreamException;

  3. public class SerialSingleton implements Serializable {

  4.     private static final long serialVersionUID = 1L;

  5.     private SerialSingleton() {

  6.     }

  7.     private static class Handler {
  8.         private static SerialSingleton instance = new SerialSingleton();
  9.     }

  10.     public static SerialSingleton getInstance() {
  11.         return Handler.instance;
  12.     }

  13.     // 解决反序列化导致不同句柄
  14.     protected Object readResolve() throws ObjectStreamException {
  15.         System.out.println("call readResolve");
  16.         return Handler.instance;
  17.     }
  18. }


测试结果:

点击(此处)折叠或打开

  1. 325040804
  2. call readResolve
  3. 325040804


从运行结果可知,添加readResolve方法后反序列化后得到的实例和序列化前的是同一个实例,单个实例得到了保证。

6  枚举类型单例模式

         和饿汉式一样,简单易懂,而且在JVM层实现了线程安全。

6.1  枚举数据类型实现单例模式

枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例

实现代码:

点击(此处)折叠或打开

  1. public enum EnumFactory {
  2.     singletonFactory;

  3.     private Singleton instance;

  4.     public Singleton getInstance() {
  5.         return instance;
  6.     }

  7.     private EnumFactory() {
  8.         instance = new Singleton();
  9.     }
  10. }

  11. class Singleton {
  12.     public Singleton() {

  13.     }
  14. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4. System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


测试结果:

点击(此处)折叠或打开

  1. 1370313450
  2. 1370313450
  3. 1370313450
  4. 1370313450
  5. 1370313450
  6. 1370313450
  7. 1370313450
  8. 1370313450
  9. 1370313450


运行结果表明单例得到了保证,但是这样写枚举类被完全暴露了,违反了“职责单一原则”。

6.2  升级版枚举数据类型实现单例模式

不暴露枚举类实现细节的封装代码,实现单一职责

实现代码:

点击(此处)折叠或打开

  1. public class ClassFactory {

  2.     private enum EnumFactory {
  3.         singletonFactory;

  4.         private Singleton instance;

  5.         private EnumFactory() {
  6.             instance = new Singleton();
  7.         }

  8.         public Singleton getInstance() {
  9.             return instance;
  10.         }
  11.     }

  12.     public static Singleton getInstance() {
  13.         return EnumFactory.singletonFactory.getInstance();
  14.     }
  15. }

  16. class Singleton {
  17.     public Singleton() {

  18.     }
  19. }


测试代码:

点击(此处)折叠或打开

  1. public class SingletonTest implements Runnable {
  2.     @Override
  3.     public void run() {
  4.         System.out.println(ClassFactory.getInstance().hashCode());
  5.     }

  6.     public static void main(String[] args) {
  7.         Thread[] threads = new Thread[10];
  8.         for (int i = 0; i < threads.length - 1; i++) {
  9.             threads[i] = new Thread(new SingletonTest());
  10.             threads[i].start();
  11.         }
  12.     }
  13. }


测试结果:

点击(此处)折叠或打开

  1. 1404419421
  2. 1404419421
  3. 1404419421
  4. 1404419421
  5. 1404419421
  6. 1404419421
  7. 1404419421
  8. 1404419421
  9. 1404419421


7  总结

饿汉式和枚举式的实现方式是比较推荐,不会出现线程安全的问题,也是解决线程安全的单例实现的有效方式。







阅读(1250) | 评论(0) | 转发(0) |
0

上一篇:开源协议

下一篇:线程同步与阻塞

给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册