查看原文
其他

单例模式--我的机器人女友

The following article is from 编程我也会 Author 詹应达

引子


2099年,程序员小帅已经35岁了,却还是单身狗一枚。

虽然说这个时代单身一辈子的人越来越多,家里人也不会催你结婚,但是小帅的思想还是比较传统,渴望着被爱。

人工智能技术在本世纪中期飞速发展,现在的机器人智力已经和人类相当,人与机器人共处的社会已经到来。

创新基因公司最近推出一款划时代的产品:机器人女友。


通过3D人工智能打印技术,能够随时随地打印出实体美女机器人,引起了社会轰动,当然最激动的还是宅男们,其中自然也包括小帅。

为了吸引顾客,创新基因公司推出了一项服务,可以把机器和材料运到客户家里,让客户在家自行打印机器人女友,看着女友“诞生”的过程。

是不是很兴奋?

小帅花了多年的积蓄,购买了最新款的女友机器人。

很快,快递员送来了几大箱东西,其中包括打印机器,机器人制作材料,和一台控制电脑。

电脑里预装了打印程序,通过电脑可以控制打印机器“制造对象”,程序代码如下:


   public class GirlFriend {

   private String name;

   publicGirlFriend(String name) {
       this.name = name;
       System.out.println("机器人女友制作完成");
   }

   publicvoidsmile() {
       System.out.println("笑一个 :-)");
   }

   publicvoidhousework() {
       System.out.println("去干家务");
   }

   publicvoidbuKeMiaoShu() {
       System.out.println(".......");
   }
}


操作也很简单,直接创建对象就行了。  
public static void main(String[] args) {
    GirlFriend girlFriend = new GirlFriend("小丽");
    girlFriend.smile();
    girlFriend.housework();
    girlFriend.buKeMiaoShu();
}

你看,马上就能干活了。

   
   机器人女友制作完成
笑一个 :-)
去干家务
.......

小帅很快发现了一个漏洞,如果材料足够,他能创造无数个对象。

很快,小帅就在黑市上购买了一套材料,回家启动机器,果然制造出了第二个机器人女友。

小帅难掩心中的兴奋,盘算着,再去黑市买几套材料回来,不就能打印很多个“女友”了?

单例模式


创新基因公司的监控系统很快就发现了这个问题,工程师们加班加点在线升级了系统。他们采用了一种叫做单例模式的设计模式来保证一台机器只能生成一个对象。


更新后的代码如下:  
public class GirlFriend {

   private static GirlFriend girlFriend;

   private String name;

   private GirlFriend(String name) {
       this.name = name;
       System.out.println("机器人女友制作完成");
   }

   /**
    * 对象通过getInstance方法获取
    * @param name
    * @return
    */
   public static GirlFriend getInstance(String name) {
       if(girlFriend == null) {
           girlFriend = new GirlFriend(name);
       }
       return girlFriend;
   }

   public void smile() {
       System.out.println("笑一个 :-)");
   }

   public void housework() {
       System.out.println("去干家务");
   }

   public void buKeMiaoShu() {
       System.out.println(".......");
   }
}
 
   
   publicstaticvoidmain(String[] args){
   GirlFriend girlFriend = GirlFriend.getInstance("小丽");
   girlFriend.smile();
   girlFriend.housework();
   girlFriend.buKeMiaoShu();
}


创新基因公司的工程师们很满意,他们开了一个盛大的party,庆祝工作的圆满完成。

小帅花了重金,在黑市上买了好几套材料,打算回家打造一个“后宫团”,想想以后的日子真是美滋滋啊。

小帅打开程序一看傻了眼,运行了好多次,获取的都是同一个对象,“后宫”梦就此破裂了吗?

小帅不甘心啊。

经过几天苦思冥想,小帅终于想到了破解方法,这就是多线程:


   publicstaticvoidmain(String[] args){
   for(int i = 0; i < 5; i++) {
       new Thread(new Runnable() {
           @Override
           publicvoidrun(){
               GirlFriend girlFriend = GirlFriend.getInstance("小丽");
               System.out.println(girlFriend);
           }
       }).start();
   }
}
 

5个线程同时运行,顺利创建了3个不同的对象。

   
   机器人女友制作完成
singleton.singleton.GirlFriend@95458f7
机器人女友制作完成
机器人女友制作完成
singleton.singleton.GirlFriend@d9d8ad0
singleton.singleton.GirlFriend@383a0ba
singleton.singleton.GirlFriend@d9d8ad0
singleton.singleton.GirlFriend@d9d8ad0

“呜。呜。呜。”,创新基因公司的报警器又响了起来,工程师们都一脸懵逼,这么完美的单例模式怎么还有破绽呢?

最后还是技术总监亲自出马,给工程师们讲解,顺便画了一幅图:


线程1和线程2判断girlFriend的时候如果都为空,就会各自创建一个对象,最后就会返回两个不同的对象了。

工程师们恍然大悟。

“谁知道如何改进吗?”技术总监问道。

懒汉式

“这个简单,在getInstance方法上加个synchronized关键字就行了!”程序员老王得意的说。

 
/**
* 对象通过getInstance方法获取
 * @param name
 * @return
 */
public synchronized static GirlFriend getInstance(String name) {
    if(girlFriend == null) {
        girlFriend = new GirlFriend(name);
    }
    return girlFriend;
}

“这样确实可以,不过,”技术总监话锋一转,“你有没有考虑过效率问题?”

“synchronized同步方法只有第一次创建对象的时候能用到,也就是说一旦创建了girlFriend对象后就用不到这个同步功能了,但是以后每次调用getInstance方法都会进入同步代码,严重降低了效率。”

技术总监犀利地指出了问题所在。

饿汉式

“还有个办法,可以用全局变量,在类加载的时候就创建对象,所以,实例的创建过程是线程安全的。”程序员小李也想出了一个办法。

 
public class GirlFriend {

   // volatile关键字保证了每个线程看到的girlFriend对象都是最新的
   private volatile static GirlFriend girlFriend;

   private String name;

   private GirlFriend(String name) {
       this.name = name;
       System.out.println("机器人女友制作完成");
   }

   /**
    * 对象通过getInstance方法获取
    * @param name
    * @return
    */
   public static GirlFriend getInstance(String name) {
       if(girlFriend == null) {
           synchronized (GirlFriend.class) {
               if (girlFriend == null) {
                   girlFriend = new GirlFriend(name);
               }
           }
       }
       return girlFriend;
   }

   public void smile() {
       System.out.println("笑一个 :-)");
   }

   public void housework() {
       System.out.println("去干家务");
   }

   public void buKeMiaoShu() {
       System.out.println(".......");
   }
}

技术总监说:“这是个办法,不过,这样的实现方式有几个问题需要考虑。”

  • 不支持延迟加载(在真正用到对象的时候,再创建实例),在类加载的时候对象就创建好了,如果对象在整个程序中一次都用不到,提前创建就浪费了。


  • 不能控制对象的数量,我们完全可以声明多个对象,比如:GirlFriend girlFriend1;GirlFriend girlFriend2;GirlFriend girlFriend3。


  • 我们可能没有足够的信息在静态初始化时,实例化每一个对象,对象的构造方法参数,可能要依赖程序后面的运算结果。

但是,我们要活学活用,如果创建对象比较耗时,等我们用到的时候再创建就会很慢,我们想在程序加载的时候提前创建好,是可以用这种方式的。

“还有没有其他方法?”技术总监追问道。

双重检测

“还有一种办法,把同步锁放到方法里面,双重检测。”程序员老王想了好久,终于想出了另一种方法。

   public class GirlFriend {

   // volatile关键字保证了每个线程看到的girlFriend对象都是最新的
   private volatile static GirlFriend girlFriend;

   private String name;

   private GirlFriend(String name) {
       this.name = name;
       System.out.println("机器人女友制作完成");
   }

   /**
    * 对象通过getInstance方法获取
    * @param name
    * @return
    */
   public static GirlFriend getInstance(String name) {
       if(girlFriend == null) {
           synchronized (GirlFriend.class) {
               if (girlFriend == null) {
                   girlFriend = new GirlFriend(name);
               }
           }
       }
       return girlFriend;
   }

   public void smile() {
       System.out.println("笑一个 :-)");
   }

   public void housework() {
       System.out.println("去干家务");
   }

   public void buKeMiaoShu() {
       System.out.println(".......");
   }
}

 

“检查girlFriend对象的时候,如果为null就进入同步代码,每个线程重新判断girlFriend对象是否为空,volatile关键字保证了每个线程看到的girlFriend对象都是最新的(在高版本的 Java中,这里已经不需要使用volatile了)。”

“如果girlFriend对象已经创建了,以后就不会进入同步代码了,这样就保证了效率。”老王解释道。

“恩,这是个好方法,这样就解决懒汉式方法的低性能和饿汉式方法的延迟加载问题,我们就采用这个方案升级代码吧。”技术总监赞许道。


总结


单例模式(Singleton Pattern):单例模式确保一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式的有三个要点:

  • 某个类只能有一个实例

  • 它必须自行创建这个实例

  • 它必须自行向整个系统提供这个实例

单例模式是一种对象创建型模式。

单例模式又名单件模式或单态模式。

单例的实现单例有下面几种经典的实现方式:

- 懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。但是,这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。

- 饿汉式

饿汉式的实现方式,在类加载的期间,就已经将静态实例初始化好了,所以,实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。

- 双重检测

双重检测的实现方式是既支持延迟加载、又支持高并发的单例实现方式。只要实例被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。


最后


工程师们马上发布了新代码,小帅忽然发现多线程也不好用了,换成10个线程,100个线程都不行,搞得满头大汗。

“啪!”小丽在身后给了小帅一个巴掌,“有老娘陪你还有不够吗!你还想要几个?”

小帅一脸懵逼,这机器人怎么还有这个功能?咋还能打人呢???

忽然,小帅想起了说明书上有个方法叫做buKeMiaoShu(),原来是这个效果啊。。。。。。

“我要退货!“


诺基亚转让操作系统版权,并以MIT协议重新分发

2021-03-25

COSS第1期:支付宝五福活动3D引擎开源;浪潮贡献低代码开发语言

2021-03-25

2102人、26家组织联名要求罢免RMS和整个FSF董事会

2021-03-25



觉得不错,请点个在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存