Java单例模式

1. 实现单例模式

  1. 饿汉模式和懒汉模式
    单例模式根据实例化时机分为饿汉模式和懒汉模式。
    饿汉模式,是指不等到单例真正使用时在去创建,而是在类加载或者系统初始化就创建好。
    懒汉模式中单例要等到第一次使用时才创建。

  2. 饿汉模式
    最简单的实现

    class Singleton{
        private static Singleton instance = new Singleton();
        private Singleton(){};
        public static Singleton getInstance(){return instance;}
    }
    

    上面是一种线程安全的实现方式,因为instance是类静态成员,会在类加载并初始化时创建,因此可以保证即便是不同线程也会获得同一份实例(这句话在有些情况下并不正确,比如通过序列化,反射的方式还是能够创建多个实例出来)。

  3. 懒汉模式

    相对于1中在加载的时候就创建,另一种则是在首次使用时创建,比如下面这种方式:

    class Singleton{
         priavte static Singleton instance = null;
         private Singleton(){};
         public static Singleton getInstance(){
             if(null == instance){
                 instance = new Singleton();
             }
             
             return instance;
         }
     }
    

    上面的这种形式,在首次调用getInstance时才会创建单例,但是它有一个问题就是,在多线程的情况下有可能会创建出多个实例化对象出来:比如线程1和线程2同时判断null == instance为true,结果进入下一步两个线程就创建两个instance出来。当然这种方式通过加锁或则使用synchronize关键字的方式就可以避免了。这里不展示对整个getInstance方法加锁的实现,而是展示另一种方式:

    3.1 两次判断,代码如下:

    class Singleton{
         priavte static volatile Singleton instance = null;
         private Singleton(){};
         public static Singleton getInstance(){
             if(null == instance){
              synchronize(Singleton.class){
                 if(null == instance){
                     instance = new Singleton();
                 }
              }
             }
             return instance;
         }
     }
    

    比起对整个getInstance方法加锁,两次判断的方式可以避免一些不必要的加锁开销。

    同时volatile关键字十分必要,多核环境下,多线程分布在多个核上,每个核心拥有各自的cache,读取数据总会尝试从cache读取。那就意味着instance = new Singleton();可能不会立即被运行在其他核心上的线程所知,导致即便instance更新后,其他线程cache中instance依然是null。volatile关键字保存每次更新都会更新到内存,同时保存其他核心上该缓存项失效,需要从内存读取。

    3.2 内部类实现延迟加载
    上面两次判断的方法依然是通过加锁的方式来保证多线程情况下的创建单一实例,回顾1的实现中,保证只有一个实例是通过jvm只初始化一次static类成员这一机制实现的,但是1中在Singleton类加载的时候就会实例化静态成员instance,这可不是我们想要的首次使用创建这一目的。为了达到这一目的,我们可以借助内部类的方式实现,下面是代码实现:

    class Singleton{
         private Singleton(){};
         
         private static class SingletonHolder{
             priavte static Singleton instance = new Singleton(); 
         }
         
         public static Singleton getInstance(){return SingletonHolder.instance;}
     }
    

    jvm加载Singleton时并不会加载其SingletonHolder,因此instance就不会被早早的创建,直到调用getInstance方法时才回加载SingletonHolder,而instance是其静态成员,jvm保证了它只此一份。

附:关于类的加载时机
「深入理解java虚拟机」一书中有介绍过类什么时候被初始化:

  1. 创建类的实例时
  2. 使用Class.forName时
  3. 访问类的静态成员
  4. 调用类的静态方法
  5. 子类初始化时,父类也会初始化

2.实现单例模式的问题

在java中创建一个对象,我们可以通过:new,clone,序列化,反射。上面单例模式的实现我们通过将构造函数私有化使得不能通过new来创建对象,但是其他的手段依然可以,下面举例说明:

  1. 反射
    通过反射我们可以访问类的私有构造函授,测试代码如下(单例代码见上面1):

    public class TestSingleton {
     public static void main(String args[]){
         try {
             Constructor cons = Singleton.class.getDeclaredConstructor();
             cons.setAccessible(true);
             Singleton instance1 = Singleton.getInstance();
             Singleton instance2 = (Singleton)cons.newInstance();
    
             System.out.println("instance1 == instance2 ?"+(instance1 == instance2));
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
     } 
    

    打印的结果如下:
    instance1 == instance2 ? false
    instance1和instance2是不同对象,因此这就破坏了单例模式,网上提供解决反射带来的问题也十分简单,只需要修改构造函数,使得它第二次以及更多次的调用抛出异常,修改构造函数如下:

    private static boolean flag = false;
    public Singleton(){
         if(false == flag){
             flag = true;
         }else{
              throw new Exception(...);
         }
    }
    

    不过java的反射有点没节操,你还是可以修改flag值,我的天。
    在《effective java》里提供一种解决之道,可以无视反射,那就是通过枚举来实现。像下面这样:

    public enum Singleton3 {
     INSTANCE;
    
     public void applaud(){
         System.out.println("haha, go home,reflection!");
     }
     }
    

    没有构造函数了。。。(跟jvm初始化枚举变量的方式有关系,当你再试图通过反射获取构造函数会抛出异常),所以再尝试通过反射获得构造函数,就会抛异常。

  2. 序列化的影响
    不考虑枚举实现单例模式,如果Singleton实现了Serializable接口,那么如果我们将Singleton序列到一个对象中去,在反序列化出来,就会导致不同的实例,请看下面代码:

    public class TestSingleton2 {
    
     public static void main(String []args){
         try {
             Singleton instance = Singleton.getInstance();
    
             //将instance序列化到文件singleton中.
             FileOutputStream fos = new FileOutputStream("singleton");
             ObjectOutputStream oos = new ObjectOutputStream(fos);
    
             oos.writeObject(instance);
    
             //从文件singleton中读出对象
             FileInputStream fis = new FileInputStream("singleton");
             ObjectInputStream ois = new ObjectInputStream(fis);
    
             Singleton instance1 = (Singleton)ois.readObject();
    
             System.out.println("instance == instance1 ? " + (instance == instance1));
    
         } catch (Exception e) {
             e.printStackTrace();
         }
    
     }
     }
    

结果显示instance和instance1为两个实例。

序列化前后产生不同对象,解决方法也很简单,jvm在反序列化时,如果该类实现的下面方法:
private Object readResolve() throw IOException
那么就会调用这个方法返回对象,以替换流中对象。因此可以在这个方法里返回Singleton的instance成员,如下:

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

推荐阅读更多精彩内容

  • 前言 本文主要参考 那些年,我们一起写过的“单例模式”。 何为单例模式? 顾名思义,单例模式就是保证一个类仅有一个...
    tandeneck阅读 2,484评论 1 8
  • java 单例模式指整个程序中只有一个某个类的实例,通常被用来代表那些本质上唯一的系统组件,比如窗口管理器或者文件...
    dcme阅读 1,042评论 0 10
  • 1 场景问题# 1.1 读取配置文件的内容## 考虑这样一个应用,读取配置文件的内容。 很多应用项目,都有与应用相...
    七寸知架构阅读 6,640评论 12 68
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • 学习了地图及大头针的使用。发现还是蛮 简单的就能实现对地图的操作。 首先,我们要了解苹果的定位组件: Wifi定位...
    晓龙歌阅读 374评论 0 0