文档章节

JAVA单例之我见

zjg23
 zjg23
发布于 2016/05/08 10:55
字数 1407
阅读 29
收藏 0

       单例模式作为设计模式中最简单的一种,是一个被说烂了的东西。但是在项目中还是会发现关于单例模式的一些错误实现,可见单例也并不是我们想象的那么简单。最近陆陆续续看了几篇关于单例的博客,很受启发,所以觉得有必要总结一下(只涉及常用的双重检查锁定、静态内部类、枚举三种单例实现方法),本文将从安全和性能两个方面阐述我对单例模式三种最佳实践的理解。不当之处,请 指正。

1、双重校验锁定

 

//Double Checked locking
public class SingletonByDCL
{
    private Map<Integer, String> configMap;
    private volatile static SingletonByDCL instance = null;

    private SingletonByDCL(Map<Integer, String> configMap)
    {
	this.configMap = configMap;
    }

    public static SingletonByDCL getInstance()
    {
	SingletonByDCL inst = instance;
	if (null == inst)
	{
	    synchronized (SingletonByDCL.class)
	    {
		inst = instance;
		if (null == inst)
		{
		    inst = new SingletonByDCL(ConfigReader.configMap);
		    instance = inst;
		}
	    }
	}
	return inst;
    }
}

 

通过synchronized和volatile实现了线程安全。其中需要注意的是实例变量一定要用volatile修饰。原因可参考Java 单例真的写对了么?。当单例对象需要被序列化时,就应该考虑单例实现的序列化安全,在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以方式单例被破坏,原理可参考单例与序列化的那些事儿。可能会有人使用反射强行调用我们的私有构造器,为了保证访问安全,可以修改构造器,让它在创建第二个实例的时候抛异常。

2、静态内部类

 

public class SingletonByInnerStaticClass
{
    private Map<Integer, String> configMap;

    private SingletonByInnerStaticClass(Map<Integer, String> configMap)
    {
	this.configMap = configMap;
    }

    private static class SingletonHolder
    {
	private static SingletonByInnerStaticClass instance = new SingletonByInnerStaticClass(ConfigReader.configMap);
    }

    public static SingletonByInnerStaticClass getInstance()
    {
	return SingletonHolder.instance;
    }
}

 

线程安全,这是 Java 运行环境自动给保证的,在加载的时候,会自动隐形的同步。在访问对象的时候,不需要同步 Java 虚拟机又会自动取消同步。对于序列化安全访问安全的保证,解决方法同“双重检查锁定”。

3、枚举

 

public enum SingletonByEnum
{
    instanse(ConfigReader.configMap);
    
    private Map<Integer,String> configMap;
    
    private SingletonByEnum(Map<Integer,String> configMap)
    {
	this.configMap = configMap;
    }

    public Map<Integer, String> getConfigMap()
    {
        return configMap;
    }
}

当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型(可用javap查看enum编译后的class文件,从而了解enum包含哪些静态资源)是线程安全的。为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定,从而保证序列化安全

 

private static void testSingletonByEnum() throws IOException, FileNotFoundException, ClassNotFoundException
    {
	// 序列化
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempfile"));
	oos.writeObject(SingletonByEnum.instanse);

	// 反序列化
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream("tempfile"));
	SingletonByEnum s = (SingletonByEnum) ois.readObject();

	// 判断是否为同一对象
	if (s == SingletonByEnum.instanse)
	{
	    System.out.println("创建的是同一个实例");
	} else
	{
	    System.out.println("创建的不是同一个实例");
	}
    }

 

创建的是同一个实例

 

 

 

同时java.lang.reflect.Constructor的newInstance()方法中有如下代码,禁止了通过反射构造枚举对象,所以枚举可以保证访问安全。关于枚举如何保证线程安全和序列化安全,可参考深度分析 Java 的枚举类型:枚举的线程安全性及序列化问题

if ((clazz.getModifiers() & Modifier.ENUM) != 0)   
   throw new IllegalArgumentException("Cannot reflectively create enum objects");

4、三种方法的性能比较

三种方法都实现了延迟加载,在8线程同时调用,每个线程调用100000000次的情况下时间对比如下:

SingletonByDCL—》845

SingletonByEnum—》90

SingletonByInnerStaticClass—》89

具体测试代码参见附件工程中的com.zjg.perf.PerformanceTest2

综上所述,Effective Java中推荐使用的枚举实现单例无论从安全还是性能都是有道理的。当然代码没有一劳永逸的写法,只有在特定条件下最合适的写法。在不同的平台、不同的开发环境(尤其是jdk版本)下,也就会有不同的最优解。比如枚举在Android平台上却是不被推荐的。在这篇Android Training中明确指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再比如双重检查锁法,不能在jdk1.5之前使用,而在Android平台上使用就比较放心了(一般Android都是jdk1.6以上了,不仅修正了volatile的语义问题,还加入了不少锁优化,使得多线程同步的开销降低不少)。

参考文章:

http://blog.jobbole.com/94074/  深度分析 Java 的枚举类型:枚举的线程安全性及序列化问题

http://www.importnew.com/18872.html  你真的会写单例模式吗——Java实现

http://www.hollischuang.com/archives/1144  单例与序列化的那些事儿

http://www.race604.com/java-double-checked-singleton/?utm_source=tuicool&utm_medium=referral Java 单例真的写对了么?

demo工程:https://git.oschina.net/zjg23/SingletonDemo

© 著作权归作者所有

共有 人打赏支持
zjg23
粉丝 19
博文 124
码字总数 41550
作品 0
济南
程序员
私信 提问
第三章 spring-bean之DefaultSingletonBeanRegistry(3)

前言 SingletonBeanRegistry是一个非常重要的接口,用于注册,获得,管理singleton对象。 SingletonBeanRegistry目前唯一的实现是DefaultSingletonBeanRegistry,DefaultSingletonBeanRegis...

鸟菜啊
2018/07/18
0
0
为什么我墙裂建议大家使用枚举来实现单例。

关于单例模式,我的博客中有很多文章介绍过。作为23种设计模式中最为常用的设计模式,单例模式并没有想象的那么简单。因为在设计单例的时候要考虑很多问题,比如线程安全问题、序列化对单例的...

2018/06/10
0
0
Java语言中单例模式的四种写法

作为设计模式理论中的Helloworld,相信学习java语言的人,都应该听说过单例模式。单例模式作为对象的一种创建模式,它的作用是确保某一个类在整个系统中只有一个实例,而且自行实例化并向整个...

相见欢
2013/02/03
0
20
Spring如何使用ThreadLocal解决线程安全问题

看“Spring企业应用开发实战”这本书,里面写到 Spring通过ThreadLocal将现有状态的变量(如Connection等)本地线程化,达到另一个层面是的“线程无关”,从而实现线程安全。 现在有这样一个...

乐山ing
2016/08/30
2.1K
6
10月31日云栖精选夜读 | Java性能优化的50个细节(珍藏版)

在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩...

yq传送门
2018/10/31
0
0

没有更多内容

加载失败,请刷新页面

加载更多

如何使用Hanlp加载大字典

问题 因为需要加载一个 近 1G 的字典到Hanlp中,一开始使用了CustomDictionay.add() 方法来一条条的加载,果然到了中间,维护DoubleArraTre 的成本太高,添加一个节点,都会很长时间,本来时...

左手的倒影
20分钟前
0
0
2018 年度新增开源软件排行榜之国产 TOP 50

2018 年开源中国社区「新增」开源软件排行榜之国产 TOP 50 终于发布了! 榜单根据 2018 年开源中国社区新收录的开源项目的关注度、活跃度,以及所属分类整理而来,相信在一定程度上反映了国内...

youfen
24分钟前
1
0
浅谈几种设计模式

Num1:单例模式 基本概念:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 常见写法: 懒汉式 public class Singleton { /* 持有私有静态实例,防止被引用,此处赋值为nul...

瑞查德-Jack
26分钟前
2
0
Time-wait状态(2MSL)一些理解

1. 编写TCP/SOCK 服务时,SO_REUSEADDR到底是什么意思? 这个套接字选项通知内核,如果端口忙,但TCP状态处于TIME_WAIT,可以重用端口。如果端口忙,TCP状态处于其他状态,重用端口时依旧指明...

Henrykin
33分钟前
0
0
数组处理

$arr = [ 0 => ['bid' => 1, 'money' => 1000, 'a' => 1, 'b' => 2, 'c' => 3], 1 => ['bid' => 1, 'money' => 1000, 'a' => 1, 'b' => 2, 'c' => 3],];$arr1 = [ ...

我才是张先生
42分钟前
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部