java 单例方法 性能_单例设计模式最优解(性能、并发、反射、序列化)

所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。

作用避免对象的多次创建,节约资源

单例设计模式的关键点私有构造函数

提供方法返回单例对象

保证多线程情况下单例依然唯一

确保反序列化的时候也不会重新构建对象上面的关键点也非必须都要坚持的点,还是需要具体场景具体分析吧。

1、最简单的实现(恶汉)public class Singleton{

private static final Singleton singleton = new Singleton();

public static Singleton getInstance(){

return singleton;

}

private Singleton(){

}

}

2、性能优化--lazy loaded(懒汉)上面的代码虽然简单,但是有一个问题----无论这个类是否被使用,都会创建一个instance对象,并且这个类还不一定会被使用,那么这个创建过程就是无用的,怎么办呢?

为了解决这个问题,我们想到的新的解决方案:public class SingletonClass {

private static SingletonClass instance = null;

public static SingletonClass getInstance() {

if(instance == null) {

instance = new SingletonClass();

}

return instance;

}

private SingletonClass() {

}

}代码的变化有俩处----首先,把 instance 设置为 null ,知道第一次使用的时候判是否为 null 来创建对象。因为创建对象不在声明处,所以那个 final 的修饰必须去掉。

我们来想象一下这个过程。要使用 SingletonClass ,调用 getInstance()方法,第一次的时候发现instance时null,然后就创建一个对象,返回出去;第二次再使用的时候,因为这个instance事static的,共享一个对象变量的,所以instance的值已经不是null了,因此不会再创建对象,直接将其返回。

这个过程就称为lazy loaded ,也就是迟加载-----直到使用的时候才经行加载。

3、同步(应对多线程问题)上面的代码很清楚,也很简单。单线程下,这段代码没什么问题,可是如果是多线程呢,麻烦就来了,我们来分析一下:

线程A希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,A就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败!

解决的办法也很简单,那就是加锁:public class SingletonClass{

private static SingletonClass instance = null;

public synchronized static SingletonClass getInstance(){

if(instance == null){

instance = new SingletonClass();

}

return instance;

}

private SingletonClass(){

}

}只要getInstance()加上同步锁,,一个线程必须等待另外一个线程创建完后才能使用这个方法,这就保证了单利的唯一性。

4、又是性能上面的代码又是很清楚也很简单的,然而,往往简单的东西不够理想。这段代码毫无疑问存在性能的问题----synchronized修饰的同步块可是要比一般的代码慢上几倍的!如果存在很多次的getInstance()调用,那性能问题就不得不考虑了?!!!

让我们来分析一下,究竟是整个方法都必须加锁,还是紧紧其中某一句加锁就足够了?我们为什么要加锁呢?分析一下lazy loaded的那种情形的原因,原因就是检测null的操作和创建对象的操作分离了,导致出现只有加同步锁才能单利的唯一性。

如果这俩个操作能够原子的进行,那么单利就已经保证了。于是,我们开始修改代码:public class SingletonClass{

private static SingletonClass instance = null;

public static SingletonClass getInstance(){

synchronized(SingletonClass.class){

if(instance == null){

instance = new SingletonClass();

}

}

return instance;

}

private SingletonClass(){

}

}首先去掉 getInstance() 的操作,然后把同步锁加载到if语句上。但是,这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要经行同步,性能的问题还是存在。如果............我们事先判断一下是不是为null在去同步呢?public class SingletonClass{

private static SingletonClass instance = null;

public static SingletonClass getInstance(){

if(instance == null){

synchronized(SingletonClass.class){

if(instance == null){

instance = new SingletonClass();

}

}

}

return instance;

}

private SingletonClass(){

}

}还有问题吗?首先判断instance是不是为null,如果为null在去进行同步,如果不为null,则直接返回instance对象。

这就是double---checked----locking 设计实现单利模式。到此为止,一切都很完美。我们用一种很聪明的方式实现了单例模式。

5、从源头检查下面我们开始说编译原理。所谓编译,就是把源代码”翻译“成目标代码----大多是是指机器代码----的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。

要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。

下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。

下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了!举例:

创建一个对象 new Object() 看似一句话,但是实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了三件事情:

1、跟实例分配内存;

2、调用类的构造函数,初始化成员字段;

3、将instance 对象指向分配的内存空间(此时instance就不是null了);

1、2、3的顺序可能不一致,所以可能会出错。

6. 解决方案

了这么多,难道单例没有办法在Java中实现吗?其实不然!在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义,用volatile修饰instance之后,能够保证instance对象每次都是从主内存读取的。说明:Java内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。这同样也导致了volatile也会有一些性能问题,不过影响还是非常小的。public class SingletonClass {

private volatile static SingletonClass instance = null;

public static SingletonClass getInstance() {

if (instance == null) {

synchronized (SingletonClass.class) {

if(instance == null) {

instance = new SingletonClass();

}

}

}

return instance;

}

private SingletonClass() {

}

}

这种方法的缺点这只是JDK1.5之后的Java的解决方案,之前版本volatile还没有被赋予这个语义功能,所以这个方案不适用于之前的老版本。

某些情况下还是会出现失效问题,在《java并发编程实践》一书中谈到了这个问题,是不赞成这种用法的。

其实,还有另外的一种解决方案,并不会受到Java版本的影响:

静态内部类方案(Effiective Java推荐的方式)public class SingletonClass {

private static class SingletonClassInstance {

private static final SingletonClass instance = new SingletonClass();

}

public static SingletonClass getInstance() {

return SingletonClassInstance.instance;

}

private SingletonClass() {

}

}在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。

由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。到这里一般就够用了,只不过如果考虑一些奇葩情况当然依然是有一些问题存在的。

1、可以反序列化创建对象。

2、可以反射调用私有构造函数创建对象。

枚举单例设计模式(最安全最简单的方式)public enum SingleInstance {

INSTANCE;

public void test(){

System.out.println(INSTANCE.name());

}

}

调用public class MainTest {

public static void main(String[] args) {

SingleInstance.INSTANCE.test();

}

}写法简单是枚举单例最大的优点,枚举在java中其实编译后生成的也是一个java类,枚举不仅能够有字段,也可以有自己的方法,最重要的是枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例,在上面几种单例创建模式实现中,在反序列的情况下他们会出现重新创建对象。

我们知道序列化可以将一个单例对象写到磁盘,然后在读取回来,从而有效的获取了一个实例,即使构造函数是私有的,反序列化操作依然可以通过特殊途径去创建一个类的新的实例。

反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的readResolve()函数,这个函数可以让开发人员控制对象反序列化。

上面几种示例如果要杜绝单例对象在反序列被重新创建的情况,就必须加入readResolve()函数,就是在这个方法里面直接将单例对象返回(复写此方法,直接返回单例对象即可),而不是新创建一个对象。(对于枚举则不会存在这个问题,枚举反序列化也不会创建新的实例【为什么:参见:枚举为什么是最好的单例,以及序列化反序列化等操作】)

另外枚举也可以避免反射创建对象(具体参见上方文章)

更多的还有容器类的单例设计管理模式,适合管理很多单例类。

容器的单例设计模式:/**

* 单例管理类

*/

public class SingletonManger {

private static Map objectMap = new HashMap<>();

/**

* 私有化管理类,防止创建多个

*/

private SingletonManger(){

}

/**

* 插入单例类

* @param key

* @param instance

*/

public static void registerService(String key,Object instance){

if(!objectMap.containsKey(key)){

objectMap.put(key,instance);

}

}

/**

* 获取单例类

* @param key

* @return

*/

public static Object getSigleInstance(String key){

return objectMap.get(key);

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值