设计模式 -- 单例模式(Singleton Pattern)


什么是单例模式?
定义:确保某一个类只有一个实例,而且可以自行实例化,并向整个系统提供这个实例
关键点:只有一个实例,且为自己创建的,为系统其他对象提供这一实例

特点
①私有的构造函数,禁止外部创建对象
②可以自行实例化的私有对象
③获取私有实例对象的公共方法

通用单例模式代码
①饿汉式单例模式,在单例类被加载时实例化一个对象,是线程安全的

public class Singleton {
//自行实例化
   private static final Singleton SINGLETON = new Singleton(); 
//限制产生多个对象
   private Singleton(){};
//对外获取私有实例对象
   public static Singleton getSingleton(){
      return SINGLETON;
   }
 //类中其他方法尽量是 static
   public static void doSomething(){}
}

②懒汉式单例模式,只在外部第一次调用公共方法时才会实例化对象,是线程不安全的

public class Singleton {
   private static  Singleton singleton  = null;
   private Singleton(){};
   public static Singleton getSingleton(){
      if (singleton == null){ ①
         singleton  = new Singleton(); ②
      }
      return singleton;
   }
   public static void doSomething(){}
}

③从JDK1.5开始,可以使用枚举类创建单例模式,有效防止反射,序列化带来的影响

public enum Singleton {  
    INSTANCE;  
}

④Lazy initialization holder class 静态字段延迟初始化,属于线程安全

public class LazyInitHolderSingleton {
   private LazyInitHolderSingleton(){};
   private static class singletonHolder{
      private static final LazyInitHolderSingleton INSTANCE = new LazyInitHolderSingleton();
   }
   public static LazyInitHolderSingleton getInstance(){
      return singletonHolder.INSTANCE;
   }
}

在②懒汉式单例模式中是会产生线程安全问题的
分析过程:当线程A执行到 ② 时,但还没获取到对象(对象初始化需要时间),这时线程B执行到 ①,会发现 singleton == null,然后线程B也会进入 ② 中,之后线程A,B都会获得一个新的对象,这就不符合单例模式要求,出现了问题。
解决方法:
①在方法上加 synchronized,线程安全

public class Singleton {
   private static  Singleton singleton  = null;
   private Singleton(){};
   public static synchronized Singleton  getSingleton(){
      if (singleton == null){
         singleton  = new Singleton();
      }
      return singleton;
   }
}

②在方法内增加 synchronized (双重检查锁),线程安全

public class Singleton {
   private static  volatile Singleton singleton  = null;
   private Singleton(){};
   public static  Singleton  getSingleton(){
      if (singleton == null){ ①
         synchronized (Singleton.class){
            if (singleton == null){
               singleton  = new Singleton();
            }
         }
      }
      return singleton;
   }
}

②相对于①来讲有以下优点:
效率提升:将 synchronized 放在if判断内部,这样就不需要每次外部调用时都进行同步(synchronized 是比较重同步锁,效率较低),只有第一次①singleton 为null时,才会进行同步,之后就不需要同步了。

因此,在面对高并发情况下,最好使用双重检查锁形式

单例模式的应用
优点:
①由于单例模式中只存在一个实例,所以可以减少内存开支,在频繁创建或销毁,且创建或销毁时无法优化性能时优势体现的最为明显。
②减少性能开销。(需要注意JVM的垃圾回收机制)
③避免资源的多重占用。
④设置全局访问点,优化与共享资源访问。

缺点
①单例模式没有接口,扩展困难
分析:单例模式要求自行实例化,那么就不能是接口或者抽象类,因为他们不可以被实例化。特殊情况下,单例模式可以实现接口,被继承等,在开发中按环境判断。
②单例模式对测试是不利的。
分析:在并行开发环境中,如果单例模式没有完成是无法进行测试的,没有接口也不能使用mock方式虚拟一个对象。
③单例模式与单一职责原则有冲突
分析:单例模式把“单例”与其他业务逻辑放在一个类中,这不符合单一指责要求

使用场景
①要求生成唯一序列号的环境
②在项目中需要一个共享访问点或共享数据:比如Web页面计数器
③创建对象需要消耗的资源过多:访问IO和数据库资源
④需要定义大量的静态常量和静态方法(比如工具类),当然也可以直接采用static方式

注意事项
①在高并发情况下,需要注意单例模式的线程同步问题,推荐使用双重检查锁。
②单例类不要实现Cloneable接口。即使是私有构造函数,依然可以通过对象Clone方式创建一个新的对象,Clone对象是不需要调用类的构造函数的。
③不要做断开单例类对象与类中静态引用的危险操作
④只使用单例类中提供的公共方法得到单列对象,不要使用反射,否则也会实例化一个新对象。具体代码如下

Class c = Class.forName(Singleton.class.getName());  
Constructor ct = c.getDeclaredConstructor();  
ct.setAccessible(true);  
Singleton singleton = (Singleton)ct.newInstance();
---------------------
防止被反射破坏可以在同步模块中增加一个全局boolean 参数判断

⑤序列化可能会对单例模式造成破坏,序列化会通过反射调用无参数的构造方法创建一个新的对象。在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

public class Singleton implements Serializable {
   //自行实例化
   private static final Singleton SINGLETON = new Singleton();
   private static final long serialVersionUID = -6698091884956053556L;
   //限制产生多个对象
   private Singleton(){};
   //对外获取私有实例对象
   public static Singleton getSingleton(){
      return SINGLETON;
   }
   private Object readResolve(){
      return SINGLETON;
   }
}

扩展延伸
(1)单例模式是只允许系统中某个类只存在一个实例,若要求一个类只能产生两三个对象呢?
分析:设置全局类实例数量,在公共方法中根据实例数量随机取得实例对象
代码重现:

//设置最大实例数量
private static final int NUM = 2;
//实例化对应数量的对象集合
private static ArrayList<Singleton>singletons = new ArrayList<Singleton>();
static {
   for (int i = 0;i<NUM;i++){
      singletons.add(new Singleton());
   }
}
//限制产生多个对象
private Singleton(){};
//对外获取私有实例对象
public static Singleton getSingleton(){
   Random random  = new Random();
   int num = random.nextInt(NUM);
   return singletons.get(NUM);
}

这种需要产生固定数量对象的模式叫做有上限的多例模式,方便系统扩展,修正单例可能存在的性能问题,提高系统的响应速度。

关于Java单例模式的一些问题:
①JVM的垃圾回收机制到底会不会回收掉长时间不用的单例模式对象?
这个问题我在网上查了很久,发现网上普遍认为答案是:不会回收。理由如下:
jvm卸载类的判定条件如下:
1,该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
2,加载该类的ClassLoader已经被回收。
3,该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
只有三个条件都满足,jvm才会在垃圾收集的时候卸载类。显然,单例的类不满足条件一,因此单例类也不会被回收。

在知乎上R大是这么说的
无论多长时间不用,只要对象有被一个活着的强引用所引用,JVM的GC就奈它不何--无法回收,要想达到长时间不用就抛弃的目的,Java提供了若干机制让程序员自行实现这样的功能。例如说结合SoftReference或WeakReference来实现可抛弃的cache。
JVM自身在看到强引用的时候只能一视同仁,无法判断应用层程序员的意图是什么。安全起见当然只能保留有被活着的强引用所引用的对象。
话说题主特意提到singleton。如果是这样的情况:

public class FooSingleton {
  public static final FooSingleton VALUE = new FooSingleton();
  private FooSingleton() {}
}

然后程序运行一段时间后,已经没有任何活的强引用引用下述任何对象:

  • FooSingleton.class
  • 加载FooSingleton的ClassLoader(defining class loader)
  • FooSingleton.VALUE所指向的单例对象
  • 加载FooSingleton的ClassLoader所加载的所有其它类也都没有活的实例并且其Class对象没有被活的强引用所引用
    那么FooSingleton类就会符合类卸载的条件,在一个有实现类卸载的JVM实现上,该类就会被卸载掉。相应的,这个类的单例对象自然也可以被回收掉。
    以HotSpot VM为例,它默认是开启了类卸载功能的,在做full GC时(或者CMS打开-XX:+CMSClassUnloadingEnabled、JDK8u40 / JDK9后的G1在完成concurrent mark后) 会卸载掉符合卸载条件的类。
    说明:虽然 static final 是强引用,但是在根源上来讲是依附在类上,而JVM对类的引用在允许卸载类的JVM里是弱引用语义的。所以:JVM-(弱引用)->类-(强引用)->FooSingleton实例,这根源上的引用是个弱引用。

我不知道答案,所以把我查到的答案放上来,供大家一起思考。

②若单例类中有finalize方法,那么与没有finalize方法的单例类在被回收上有无明显差异?
还是根据R大在知乎上的回答
答:一个类有finalize方法也只是会让它的实例延迟一点才可以被回收(finalize后没被复活的话才可以下次GC被回收)。当所有实例都死光光后,有没有finalize方法对一个类能不能被卸载没有任何影响。JVM不会“提前回收”任何有活着的强引用所引用的东西。有finalize就按finalize的办,并没有啥特别的地方。

③在一个JVM中会出现多个单例对象么
答:在分布式系统,多个类加载器,以及序列化的情况下,会产生多个单例。在JVM中通过单例类中的公共方法只能得到同一个单例,除非利用反射或者实现Cloneable接口。

④单例类可以被继承吗
对于这种通过私有化构造函数,静态方法提供实例的单例类而言,是不支持继承的。

参考书籍:设计模式之禅 --- 秦小波 著
参考文章:
singleton模式四种线程安全的实现
如何防止单例模式被JAVA反射攻击
单例与序列化的那些事儿
单例模式讨论篇:单例模式与垃圾回收
23种设计模式(1):单例模式
JVM的垃圾回收机制到底会不会回收掉长时间不用的单例模式对象

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260