JAVA设计模式之单例模式

单例模式

单例模式在程序设计中使用的频率非常之高,其设计的目的是为了在程序中提供唯一一个对象(保证只被构造一次),例如写入日志的log对象,windows的任务管理器实现(只能打开一个)。这里主要介绍单例模式使用Java的实现(包括饿汉式及懒汉式)。

实现

这里使用Log类作为例子,Log对象需要在程序中只有一个对象且只初始化一次。

饿汉式

饿汉式的单例模式理解起来是比较容易的,就是在单例类加载的时候就初始化需要单例的对象。实现也比较容易。

public class Singleton{
    private static Log logObj = new Log();

    public static Log getInstance(){
        return logObj;
    }
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

懒汉式

如果logObj需要占用很大的内存,如果一开始就初始化logObj,那么会占用大量的内存。此时,有人就想,如果我在想用的时候再初始化Log类的对象,像懒汉一样,只有用到的时候再初始化,需要怎么设计呢?

实现一(非线程安全版本)


public class Singleton{
    private static Log logObj = null;

    public static Log getInstance(){
        if(logObj == null){
            logObj = new Log();
        }
        return logObj;
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

实现二(线程安全版本)


public class Singleton{
    private static Log logObj = null;

    public static synchronized Log getInstance(){
        if(logObj == null){
            logObj = new Log();
        }
        return logObj;
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

为了实现线程安全,这个版本的实现牺牲了一定的效率,如果logObj已经初始化,那么其他线程还需要同步的进入getInstance方法,会造成效率的损失。于是,有些人实现了下面的版本。

实现三(错误版本)

public class Singleton{
    private static Log logObj = null;

    public static Log getInstance(){
        if(logObj == null){
            synchronized(Singleton.class){
                if(logObj == null){
                    logObj = new Log();
                }
            }
        }
        return logObj;
    }
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

乍看起来上面的版本是没问题的,如果某个线程A发现logObj 还没初始化,那么就进入同步块初始化logObj,如果在这期间有其他线程B进入,那么线程B就会等待进入同步块,等待A 线程退出同步块,logObj 已经初始化了,B 线程进入同步块后发现logObj 不为null,退出同步块,不再初始化logObj 。 这样既实现了线程安全,又兼顾了效率,确实是很聪明的编码方式。但是问题来了,由于指令重排序的存在,会导致Log在完全初始化之前logObj就已经不为null。这样其他线程可能会得到未完全初始化的对象。

解决方法

  1. JDK1.5版本后扩展了volitile语义,可以保证上述代码的正确性,为此只要将logObj 声明为volitile即可(volitile之前只是保证内存的可见性而已)。

  2. 使用静态内部类。

加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

并且jvm会保证类加载的线程安全问题,所以利用这个特性可以写出兼顾效率与保证线程安全的版本。

实现四(兼顾效率与线程安全的版本)

public class Singleton{
    static class LogHolder{
        static Log logObj = new Log();
    }

    public static Log getInstance(){
        return LogHolder.logObj;
    }
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这样在Singleton类加载时,并不会加载LogHolder,也就不会初始化Log,如果有线程访问getInstance方法,那么jvm会首先加载LogHolder类,并保证初始化logObj,最后返回logObj

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值