1,单例模式:
为了限制该类对象被随意的创建,需要保证该类构造方法是私有的,这样外部类就无法创建该类型的对象了,另外,为了方便给客户对象提供对此单例对象的使用,给它提供一个全局访问点。
2,实现方式:
通常单例模式在Java语言中,有两种构建方式:
懒汉式—线程不安全:最基础的实现方式,线程上下文单例,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。 懒汉式—线程安全:加上synchronize之类保证线程安全的基础上的懒汉模式,相对性能很低,大部分时间并不需要同步 饿汉方式。指全局的单例实例在类装载时构建。 双检锁式。在懒汉式基础上利用synchronize关键字和volatile关键字确保第一次创建时没有线程间竞争而产生多个实例,仅第一次创建时同步,性能相对较高 登记式。作为创建类的全局属性存在,创建类被装载时创建 枚举。java中枚举类本身也是一种单例模式
3,单例中懒汉和饿汉的本质区别在于以下几点:
(1)饿汉式: 线程安全的,在类创建的同时就已经创建好一个静态对象供系统使用,以后不再改变。
懒汉式:如果在创建实例对象时不加上synchronized则会导致对象的访问不是线程安全的。
(2)实现方式上划分:
懒汉式是延迟加载,它在需要的时候才会创建对象,而饿汉式是在虚拟机启动的时候就会创建。
饿汉式无需关注多线程问题,写法简单明了,能用则用。
注意:它是加载类时创建实例,所以如果是工厂模式,缓存了很多实例,那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用都一块创建。
(3)两者建立单例对象的时间不同。“懒汉式”是在你真正用到的时候才去建这个单例对象,“饿汉式”是在不管用不用得上,一开始就建立这个单例对象。
4,优缺点对比:
饿汉式:没有加锁,执行效率会提高。但是类加载时就初始化,浪费内存。它基于 classloder 机制避免了多线程的同步问题,
懒汉式:线程不安全。
5,简单的单例模式
(1)饿汉模式:
public class HungrySingleton { //保存该类对象的实例,饿汉式的做法:在声明的同时初始化该对象 private static HungrySingleton instance=new HungrySingleton(); //将构造函数私有化,不对外提供构造函数 private HungrySingleton(){} //对外提供访问该类对象的方法 public static HungrySingleton getInstance(){ return instance; } }
(2)懒汉模式:
出于性能等方面的考虑,采用延迟实例化单例对象(static属性在加载类时就被初始化),只有第一次使用该类的实例时才去实例化。-------------延迟创建
/** * 非线程安全的懒汉式 */ public class UnThreadSafeLazySingleton { private static UnThreadSafeLazySingleton singleton=null; private UnThreadSafeLazySingleton(){} public static UnThreadSafeLazySingleton getInstance(){ if(singleton==null){ singleton=new UnThreadSafeLazySingleton(); } return singleton; } }
6,线程不安全的解决办法:
在高并发环境中,getInstance()方法返回的多个指向不同该类的实例,这样在没有自动内存回收的平台会产生内存泄漏。
(1) 加同步锁--------synchronized 同步方法
public class TheadSafeSingelton { private static TheadSafeSingelton singleton=null; private TheadSafeSingelton(){} public static synchronized TheadSafeSingelton getInstance(){ if(singleton==null){ singleton=new TheadSafeSingelton(); } return singleton; } }
(2)加同步块------双重检测 Double-Check Locking
synchronized 是重量级锁,在高并发访问情况下,很影响性能,所以缩小锁的范围来提高性能
// 双重检测 public class DoubleCheckSingleton { private volatile static DoubleCheckSingleton singleton=null; private DoubleCheckSingleton(){} public static DoubleCheckSingleton getInstance(){ if(singleton==null){//检查是否已经被创建 synchronized (DoubleCheckSingleton.class){ //同步块 if (singleton==null){//再次检测是否被创建----双重检测 singleton=new DoubleCheckSingleton(); } } } return singleton; } }
new Singleton()分为三步,1、分配内存空间,2、初始化对象,3、设置instance指向被分配的地址。然而指令的重新排序,可能优化指令为1、3、2的顺序。如果是单个线程访问,不会有任何问题。但是如果两个线程同时获取getInstance,其中一个线程执行完1和3步骤,此时其他的线程可以获取到instance的地址,在进行if(instance==null)时,判断出来的结果为false,导致其他线程直接获取到了一个未进行初始化的instance,这可能导致程序的出错。所以用volatile修饰instance,禁止指令的重排序,保证程序能正常运行。(Bug很难出现,没能模拟出来)。
参考地址:volatile
(3) 既要保证线程安全又要延迟加载的效果还可以采用 持有者按需初始化模式(Initilization on demand holer)
/** * Initialization on demand holer 持有者按需初始化 * @Author: wxw * @create: 2019-10-20-20:53 * 线程安全的延迟加载的单例初始化 */ public class LazyLoadedSingleton { private LazyLoadedSingleton(){} private static class LazyHolder{ //持有单例类 private static final LazyLoadedSingleton singleton=new LazyLoadedSingleton(); } public static LazyLoadedSingleton getInstance(){ return LazyHolder.singleton; } }
说明:由于Jvm加载 LazyLoadedSingleton 类时,该类没有static的属性,所以加载完成后可以返回,只有第一次使用getInstance()方法时,Jvm才会加载LazyHolder类,由于它包含了一个static属性的singleton实例
所以会首先初始化这个变量,这样并不会出现并发问题,这样既保证了线程安全又支持懒加载的单例模式。
7,单实例singleton的序列化问题
(1)问题: 如果单例类实现了Seriaizable接口,在默认情况下每次反序列化都会创建一个新的实例对象,这样一个系统会使用多个对象共使用。
解决方案:在readResolve()方法中替换掉反序列化出来的那个新实例,让其指向内存中那个单例对象即可。
(2)readResolve()方法:此方法在反序列化完成之前执行
public class SerialibleSingleton implements Serializable { private static SerialibleSingleton singleton=new SerialibleSingleton(); private SerialibleSingleton(){} public static SerialibleSingleton getInstance(){ return singleton; } //反序列化完成之前执行,为了替换掉反序列化出来的那个新实例 private Object readResolve(){ return singleton; } }
9,单例模式优缺点:
优点:
1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
3.提供了对唯一实例的受控访问。
4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
5.允许可变数目的实例。
6.避免对共享资源的多重占用。
缺点:
1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
使用注意事项:
1.使用时不能用反射模式创建单例,否则会实例化一个新的对象
2.使用懒单例模式时注意线程安全问题
3.饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)
参考博客:1,线程安全分析单例
2,单例模式概述