作者 |静幽水
责编 | Elle
问题背景
某公司老板在招程序员时承诺帮助解决单身问题,给程序员分配一个女朋友,于是单身的小强毫不犹豫就去应聘了,并被顺利录用。那么我们怎么用代码来模拟一下呢?首先定义一个女朋友的类,拥有两个属性,姓名和年龄:
接着程序员小强就可以new出来一个女朋友的实例了,只需要传进去姓名和年龄就可以了,如下:
打印出的结果是GirlFriend
有何问题
突然有一天,程序员小强已经不满足只有一个女朋友了,于是他私自new出了多个女朋友对象出来,如下:
打印结果如下:
但是不久就被老板发现了,因为内存中存在多个女朋友实例对象,严重浪费了公司的资源,老板决定只能给小强分配一个女朋友,老板绞尽脑汁,终于想出了应对方法。
解决方法
老板发现,问题的根源就是不能把创造女朋友的权限交给小强,应该给他一个创造好的对象,并且姓名和年龄也不能由小强来决定,不然他肯定只要18岁的。而是要把创建实例的权限收回,让类自身负责自己类实例的创建。
来看看代码如何实现:
主要的核心思想有三点:
1.定义一个变量来存储创建好的类实例;
2.私有化构造函数,防止外部new该对象;
3.对外提供一个能获取到该对象的方法。
程序员小强该如何获取呢:
直接使用GirlFriend类调用获取对象的getGirlFriend方法获取到实例对象,打印结果如下:
模式讲解
上面这种解决方案就是单例模式(Singleton),单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
通用类图如下:
Singleton:负责创建单例类自己的唯一实例,并提供一个getInstance的方法让外部来访问这个类的唯一实例。
单例模式功能:保证类在运行期间只允许被创建一个实例。有懒汉式和饿汉式两种实现方式。
懒汉式:上面的代码就是懒汉式的实现方式,顾名思义,懒汉式指只有当该实例被使用到的时候才会创建,通过三个步骤就可以实现懒汉式:
1.私有化构造方法:防止外部使用。
2.提供获取实例的方法:全局唯一的类实例访问点。
3.把获取实例的方法改为静态:因为只有静态的方法才能直接通过类名来调用,否则就要通过实例调用,这就陷入了死循环。
完整代码:
这里使用到了synchronized用来保证线程安全,如果不加会带来什么问题呢?比如两个线程A和B,就有可能导致并发的问题,如图所示:
这种情况就会创建出两个实例出来,单例模式也就失效了。加上synchronized虽然能保证线程安全,但是却降低了访问速度,影响了性能,可以考虑使用双重检查加锁来解决这个问题,双重检查加锁意思是并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法之后先检查实例是否存在,如果不存在才进入同步块。这是第一重检查。进入同步块之后再检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。
代码实现:
这种方式即可以安全的创建线程,又不会对性能造成太大的影响。
饿汉式:所谓饿汉式也就是在类加载的时候直接new出一个对象来,不管以后用不用得到,是一种以空间换取时间的策略。代码也非常简单:
单例模式作用范围:目前Java里面实现的单例是一个虚拟机的范围,虚拟机在通过自己的`ClassLoader`装载饿汉式实现的单例类时就会创建一个类实例。如果一个虚拟机中有多个类加载器或者一个机器中有多个虚拟机,那么单例就不再起作用了。
单例模式优缺点:
1.节约内存资源;
2.时间和空间:懒汉式是以时间换空间,饿汉式是以空间换时间;
3.线程安全:不加同步synchronized的懒汉式是线程不安全的,而饿汉式是线程安全的,因为虚拟机只会装载一次,并且在装载的时候是不会发生并发的。加上synchronized和双重检查加锁也能保证懒汉式的线程安全。
新的问题
由于小强工作很卖命,公司业绩发展的不错,老板决定再招一名程序员,应聘者小华也是一个单身汉,老板也承诺会给他分配女朋友,单是问题来了,之前的GirlFriend只能new出一个对象,总不能让小强和小华共用一个对象吧。于是要想办法实现一个可以提供两个实例的GirlFriend类。
其实思路很简单,只需要通过Map来缓存实例即可,代码如下:
程序员类进行获取女朋友实例,如下:
上面代码获取了四次,看看打印的结果如何:
可以看出,第一次和第三次是一样的,第二次和第四次是一样的,一共就只有两个对象,解决了这个问题。但是如何判断哪个女朋友实例是小强的哪个是小华的呢?一种简单的方法是通过给获取实例的函数getGirlFriend传参,比如小强获取的时候传如number = 1,小华的number = 2。
相关扩展
在Java中还用一种更好的单例实现方式,既能够实现延迟加载,又能够实现线程安全。这种解决方案被称为Lazy initialization holder class模式,这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
类级内部类:
1.类级内部类指的是有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
2.类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可以直接创建。
3.类级内部类中可以定义静态方法。在静态方法中能够引用外部类中的静态成员方法或者成员变量。
4.类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。
缺省同步锁:在某些情况下,JVM已经隐含地执行了同步,不需要自己进行同步控制了,这些情况包括:
1.由静态初始化器初始化数据时。
2.访问final字段时
3.在创建线程之前创建对象时
4.线程可以看见它将要处理的对象时。
思路:使用静态初始化器的方式,由jvm保证线程安全。但是这样就像饿汉式的实现方式了,浪费一定的空间。采用类级内部类,在这个类级内部类里面创建对象实例,只要不使用这个类级内部类,就不会创建实例对象。
代码:
当getInstance方法第一次被调用的时候,它第一次读取 SingletonHolder.instance导致SingletonHolder类得到初始化,从而创建Singleton实例。
枚举实现单例:
1.Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。
2.Java枚举类型的基本思想是通过共有的静态fianl域为每个枚举常量导出实例的类。
3.从某种角度将,枚举是单例的泛型化,本质上是单元素的枚举。
代码:
使用枚举来实现单例控制更加简洁,而且无偿地提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化。
在Spring中,每个Bean默认就是单例的,这样的优点是Spring容器可以管理这些Bean的生命周期,决定什么时候创建出来,什么时候销毁,销毁的时候如何处理等等。
使用单例模式需要注意的就是JVM的垃圾回收机制,如果我们的一个单例对象在内存中长久不使用,JVM就认为这个对象是一个垃圾,在CPU资源空闲的情况下该对象会被清理掉,下次再调用时就需要重新产生一个对象。
声明:本文为作者投稿,版权归作者个人所有。
【End】
热 文推 荐
领取专属 10元无门槛券
私享最新 技术干货