单例模式是指确保在任何情况下,一个类只有一个实例,并提供一个全局的访问点。这样减少了内存开销,也避免了对资源的多重占用。单例模式看起来简单,实现起来也很简单。缺点是没有接口,扩展困难,若要扩展单例对象,只能修改代码,没有其他途径。
一、饿汉式单例:加载类时创建类对象,绝对线程安全,在线程还未出现以前就实例化了,不存在线程安全问题。
优点:没有任何锁,执行效率高,从用户体验上来说,比懒汉式好。
缺点:类加载时就初始化,不管用与不用,都占着内存空间,浪费了内存,当实例多时,就严重浪费内存。
二、懒汉式单例:被外部类调用时内部类才加载。有线程安全问题存在。
解决线程安全方式:
1:访问点方法加sycronize关键字,但当线程数量比较多时,CPU分配压力上升,会导致大批线程阻塞,从而使程序性能大幅下降。
2:双重检查锁,在访问点内部加sycronize关键字,可能导致方法内部阻塞,程序性能优于第一种方法。
3:使用静态内部类,在内部类里面初始化实例,利用了调用方法前先实例化内部类的特性,程序性能更好。
防止利用反射破坏单例:虽然构造方法私有化了,但还是可以通过反射创建实例,这样就破坏了单例对象的唯一性。
解决方法:构造方法检查,若实例 !=null,抛出异常,不能创建实例对象。
三、单例实现方式
饿汉式
懒汉式
序列化式 写readResolve(),可避免序列化破坏单例
枚举式:是最推荐的写单例的方式。
容器式
threadlocal线程单例:threadlocal不能保证其创建的对象是全局唯一,但可以保证单个线程内是唯一的,天生的线程安全。
经过多线程测试发现,单个线程的实例对象总是唯一的,多个线程的实例是不同的。通过源码发现,threadlocal将所有对象都放到threadlocalmap中,key是
线程,value是对象,以空间换时间来实现线程间隔离的。
伪线程安全,在一个线程内部,线程是安全的,只有一个对象。在一个线程外部,线程不安全,可创建多个对象。
对threadlocal的应用:Spring源码中,使用它来达到多数据源的动态切换。
三、其他可能出现问题:
1.有时单例需被序列化写入磁盘,然后再从磁盘反序列化读出实例对象,此时会创建一个新的对象,破坏了单例对象唯一性,怎么办?
A1:从JDK源码中,可以看到只需增加readResolve方法就可以解决。JDK考虑到这种情况,已经解决,但虽然返回了原先的实例对象,但从源码可以看到它中
途有重新创建新的实例对象,浪费了内存资源,显然,这种不是最好的解决方式。
A2:注册式单例:注册式单例也叫登记式单例,是把每一个实例都注册到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法,
1)枚举:是天生的写单例的方式,不存在线程安全问题,自动解决序列化反序列化破坏单例,用反射破坏单例的各种问题。特点:简单好用。
注:将代码反编译查看源码就会发现,枚举式单例在静态代码块中就给实例赋值,是饿汉模式的实现。
序列化不能破坏单例的原因:查看JDK源码发现,枚举类型实际通过类名和class枚举对象找到一个唯一的枚举对象。因此,枚举对象不能被类
加载器多次加载。
不能被反射破坏的原因:JDK不让枚举类用反射实例化。
2)容器缓存:容器缓存用于创建实例非常多的情况,便于管理。但它是非线程安全的,可通过加锁实现安全。