深入分析Java单例模式的各种方案
单例模式
Java内存模型的抽象示意图:

所有单例模式都有一个共性,那就是这个类没有自己的状态。也就是说无论这个类有多少个实例,都是一样的;然后除此者外更重要的是,这个类如果有两个或两个以上的实例的话程序会产生错误。
非线程安全的模式
public class Singleton {
  private static Singleton instance;
  private Singleton(){
  }
  public static Singleton getInstance() {
    if (instance == null) //1:A线程执行
      instance = new Singleton(); //2:B线程执行
    return instance;
  }
}
普通加锁
public class SafeLazyInitialization {
    private static Singleton instance;
    public synchronized static Singleton getInstance() {
        if (instance == null)
            instance = new Singleton();
        return instance;
    }
}
出于性能考虑,采用双重检查加锁的模式
双重检查加锁模式
public class Singleton{
  private static Singleton singleton;
  private Singleton(){
  }
  public static Singleton getInstance(){
    if(null == singleton){  //第一次检查
      synchronized(Singleton.class){  //加锁
        if(null == singleton){  //第二次检查
          singleton = new Singleton();//问题的根源出在这里
        }
      }
    }
    return singleton;
  }
}
双重检查加锁模式相对于普通的单例和加锁模式而言,从性能和线程安全上来说都有很大的提升和保障。然而双重检查加锁模式也存在一些隐蔽不易被发现的问题。首先我们要明白在JVM创建新的对象时,主要要经过三个步骤。
- 分配内存
 - 初始化构造器
 - 将对象指向分配的内存地址
 
这样的顺序在双重加锁模式下是么有问题的,对象在初始化完成之后再把内存地址指向对象。
问题的根源
但是现代的JVM为了追求执行效率会针对字节码(编译器级别)以及指令和内存系统重排序(处理器重排序)进行调优,这样的话就有可能(注意是有可能)导致2和3的顺序是相反的,一旦出现这样的情况问题就来了。
java源代码到最终实际执行的指令序列:

前面的双重检查锁定示例代码的(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:
memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
instance = memory;     //3:设置instance指向刚分配的内存地址
上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的,详情见参考文献1的“Out-of-order writes”部分)。2和3之间重排序之后的执行时序如下:
memory = allocate();   //1:分配对象的内存空间
instance = memory;     //3:设置instance指向刚分配的内存地址
                       //注意,此时对象还没有被初始化!
ctorInstance(memory);  //2:初始化对象

多线程并发执行的时候的情况:

解决方案
基于Volatile的解决方案
先来说说Volatile这个关键字的含义:
- 可以很好地解决可见性问题
 - 但不能确保原子性问题(通过
 synchronized进行解决)- 禁止指令的重排序(单例主要用到此JVM规范)
 
Volatile 双重检查加锁模式
public class Singleton{
  private volatile static Singleton singleton;
  private Singleton(){
  }
  public static Singleton getInstance(){
    if(null == singleton){
      synchronized(Singleton.class){
        if(null == singleton){
          singleton = new Singleton();
        }
      }
    }
    return singleton;
  }
}
基于类初始化的解决方案
利用静态内部类的方式来创建,因为静态属性由JVM确保第一次初始化时创建,因此也不用担心并发的问题出现。当初始化进行到一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以singleton仍然是单例的。

这个方案的实质是:允许“问题的根源”的三行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。
静态内部类的方式
public class Singleton{
  private Singleton(){}
  public static Singleton getInstance(){
    return InnerClassSingleton.singleton;
  }
  private class InnerClassSingleton{
    protected static Singleton singleton = new Singleton();
  }
}
然而,虽然静态内部类模式可以很好地避免并发创建出多个实例的问题,但这种方式仍然有其存在的隐患。
存在的隐患
- 一旦一个实例被持久化后重新生成的实例仍然有可能是不唯一的。
 - 由于java提供了反射机制,通过反射机制仍然有可能生成多个实例。
 
序列化和反序列化带来的问题:反序列化后两个实例不一致了。
private static void singleSerializable() {
    try (FileOutputStream fileOutputStream=new FileOutputStream(new File("myObjectFilee.txt"));
         ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);) {
//            SingletonObject singletonObject = SingletonObject.getInstance();
//            InnerClassSingleton singletonObject = InnerClassSingleton.getInstance();
        EnumSingleton singletonObject = EnumSingleton.INSTANCE;
        objectOutputStream.writeObject(singletonObject);
        objectOutputStream.close();
        fileOutputStream.close();
        System.out.println(singletonObject.hashCode());
    } catch (IOException e) {
        e.printStackTrace();
    }
    try (FileInputStream fileInputStream=new FileInputStream(new File("myObjectFilee.txt"));
         ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);) {
//            SingletonObject singleTest=(SingletonObject) objectInputStream.readObject();
//            InnerClassSingleton singleTest=(InnerClassSingleton) objectInputStream.readObject();
        EnumSingleton singleTest=(EnumSingleton) objectInputStream.readObject();
        objectInputStream.close();
        fileInputStream.close();
        System.out.println(singleTest.hashCode());
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}
问题点及解决办法
ObjectInputStream中的readOrdinaryObject。
if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        handles.setObject(passHandle, obj = rep);
    }
}
调用自定义的readResolve方法
protected Object readResolve(){
    System.out.println("调用了readResolve方法!");
    return  InnerClassSingleton.getInstance();
}
通过反射机制获取到两个不同的实例
private static void attack() {
    try {
        Class<?> classType = InnerClassSingleton.class;
        Constructor<?> constructor = classType.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        InnerClassSingleton singleton = (InnerClassSingleton) constructor.newInstance();
        InnerClassSingleton singleton2 = InnerClassSingleton.getInstance();
        System.out.println(singleton == singleton2);  //false
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}
解决方案 : 私有构造方法中进行添加标志判断。
private InnerClassSingleton() {
    synchronized (InnerClassSingleton.class) {
        if (false == flag) {
            flag = !flag;
        } else {
            throw new RuntimeException("单例模式正在被攻击");
        }
    }
}
单例最优方案,枚举的方式
枚举实现单例的优势
- 自由序列化;
 - 保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量);
 - 线程安全;
 
public enum Singleton {
    INSTANCE;
    private Singleton(){}
}
Hibernate的解决方案
通过ThreadLocal的方式
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
public class HibernateSessionFactory {
    private static String CONFIG_FILE_LOCATION = "/hibernate.cfg.xml";
    private static final ThreadLocal threadLocal = new ThreadLocal();
    private static Configuration configuration = new Configuration();
    private static org.hibernate.SessionFactory sessionFactory;
    private static String configFile = CONFIG_FILE_LOCATION;
    static {
       try {
           configuration.configure(configFile);
           sessionFactory = configuration.buildSessionFactory();
       } catch (Exception e) {
           System.err.println("%%%% Error Creating SessionFactory %%%%");
           e.printStackTrace();
       }
    }
    private HibernateSessionFactory() {
    }
    public static Session getSession() throws HibernateException {
       Session session = (Session) threadLocal.get();
       if (session == null || !session.isOpen()) {
           if (sessionFactory == null) {
              rebuildSessionFactory();
           }
           session = (sessionFactory != null) ? essionFactory.openSession() : null;
           threadLocal.set(session);
       }
       return session;
    }
// Other methods...
}
参考文档:
深入分析Java单例模式的各种方案的更多相关文章
- 用java单例模式实现面板切换
		
1.首先介绍一下什么是单例模式: java单例模式是一种常见的设计模式,那么我们先看看懒汉模式: public class Singleton_ { //设为私有方法,防止被外部类引用或实例 priv ...
 - 深入分析Java Web技术内幕(修订版)
		
阿里巴巴集团技术丛书 深入分析Java Web技术内幕(修订版)(阿里巴巴集团技术丛书.技术大牛范禹.玉伯.毕玄联合力荐!大型互联网公司开发应用实践!) 许令波 著 ISBN 978-7-121- ...
 - 深入分析 Java 中的中文编码问题
		
登录 (或注册) 中文 IBM 技术主题 软件下载 社区 技术讲座 打印本页面 用电子邮件发送本页面 新浪微博 人人网 腾讯微博 搜狐微博 网易微博 Digg Facebook Twitter Del ...
 - [转]深入分析 Java 中的中文编码问题
		
收益匪浅,所以转发至此 原文链接: http://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/ 深入分析 Java 中的中文编码问题 编 ...
 - 深入分析Java Web技术(2) IO
		
IO是当今Web面临的主要问题之一,可以说,大部分web应用的瓶颈都是IO的瓶颈. Java的IO类是java.io.它包含有80多个类,分为4大部分: 基于字节操作: InputStream,Out ...
 - 深入分析Java Web技术(1)
		
BS网络模型的基本过程: 当我们在浏览器中输入"www.google.com"的时候,首先会请求DNS服务器对域名进行解析成都应的IP地址,然后根据这个IP地址在互联网上找到谷歌的 ...
 - 【深入】java 单例模式(转)
		
[深入]java 单例模式 关于单例模式的文章,其实网上早就已经泛滥了.但一个小小的单例,里面却是有着许多的变化.网上的文章大多也是提到了其中的一个或几个点,很少有比较全面且脉络清晰的文章,于是,我便 ...
 - 深入分析 Java I/O 的工作机制--转载
		
Java 的 I/O 类库的基本架构 I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道.在当今这个数据大爆炸时代, ...
 - 深入Java单例模式(转)
		
深入Java单例模式 源自 http://devbean.blog.51cto.com/448512/203501 在GoF的23种设计模式中,单例模式是比较简单的一种.然而,有时候越是简单的东西越容 ...
 
随机推荐
- request和response对象的具体介绍
			
Request 对象 - request 对象表示 HTTP 请求,包含了请求查询字符串,参数,内容,HTTP 头部等属性.常见属性有: req.app:当callback为外部文件时,用req ...
 - warshall、
			
#include<iostream> int mian() { ][],b[][],c[][]; int i,j,k; cout<<"input the Boolea ...
 - Git学习之路(6)- 分支操作
			
▓▓▓▓▓▓ 大致介绍 几乎所有的版本控制系统都会支持分支操作,分支可以让你在不影响开发主线的情况下,随心所欲的实现你的想法,但是在大多数的版本控制系统中,这个过程的效率是非常低的.就比如我在没有学习 ...
 - 本地Solr服务器搭建
			
一.Solr官网下载http://lucene.apache.org/solr/下载Solr项目文件 在该项目文件中,可以找到我们在本地环境下运行Solr服务器所需要的资源文件,在这里我们以4.10. ...
 - 【转】44款Java 网络爬虫开源软件
			
原帖地址 http://www.oschina.net/project/lang/19?tag=64&sort=time 极简网络爬虫组件 WebFetch WebFetch 是无依赖极简网页 ...
 - FineUIMvc随笔 - 不能忘却的回发(__doPostBack)
			
声明:FineUIMvc(基础版)是免费软件,本系列文章适用于基础版. 用户反馈 有网友在官方论坛抛出了这么一个问题,似乎对 FineUIMvc 中的浏览器端与服务器端的交互方式很有异议. 这里面的关 ...
 - iOS 图片旋转方法
			
iOS 图片旋转方法 通过 CGImage 或 CIImage 旋转特定角度 UIImage可通过CGImage或CIImage初始化,初始化方法分别为init(cgImage: CGImage, s ...
 - mfc---单文档工程添加消息响应
			
写消息映射:.h中些函数头文件afx_mag … .cpp中写函数体 .cpp中写消息映射 给toolbar添加消息: .h中添加头文件afx_msg …. .cpp中添加函数体,消息映射ON_COM ...
 - 搭建hibernate环境
			
Hibernate概述什么是框架1 写程序,使用框架之后,帮我们实现一部分功能,使用框架好处,少写一部分代码实现功能 什么是hibernate框架(重点)1 hibernate框架应用在javaee三 ...
 - linux网络编程1  最简单的socket编程
			
下面是socket编程的服务器端 先看一个图,1 #include<stdio.h> #include<stdlib.h> #include<string.h> # ...