内蒙古自治区建设厅官方网站,建设部网站防排烟规范,网站根目录文件名,营销软件激活码商城这是 一道面试常考题#xff1a;#xff08;经常会在面试中让手写一下#xff09;
什么是单例模式
【问什么是单例模式时#xff0c;不要答非所问#xff0c;给出单例模式有两种类型之类的回答#xff0c;要围绕单例模式的定义去展开。】
单例模式是指在内存中只会创建…这是 一道面试常考题经常会在面试中让手写一下
什么是单例模式
【问什么是单例模式时不要答非所问给出单例模式有两种类型之类的回答要围绕单例模式的定义去展开。】
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时为了防止频繁地创建对象使得内存飙升单例模式可以让程序仅在内存中创建一个对象让所有需要调用的地方都共享这一单例对象。
单例模式Singleton Pattern是一种常用的设计模式保证一个类在内存中只有一个实例并提供一个全局访问点。单例模式通常用于管理共享资源、控制全局状态或提供全局服务。 单例模式有两种类型
懒汉式在真正需要使用对象时才去创建该单例类对象饿汉式在类加载时已经创建好该单例对象等待被程序使用
单例模式实现方法
一、饿汉式单例在类初始化时就已经自行实例化了 public class Singleton {//私有静态成员变量private static Singleton instance new Singleton();//私有构造方法private Singleton(){}//公有静态访问方法public static Singleton getInstance(){return instance;}}注意上面的代码在第2行已经实例化好了一个Singleton对象在内存中不会有多个Singleton对象实例存在类在加载时会在堆内存中创建一个Singleton对象当类被卸载时Singleton对象也随之消亡了。 当然可以改为静态方块来执行实例化语句 private static Singleton instance null static{ instance new Singleton(); } 二、懒汉式单例在第一次调用实例的时候才实例化
如果两个线程同时判断singleton为空那么它们都会去实例化一个Singleton对象这就变成双例了就不是单例了所以可以在方法上加锁或类 对象上 加锁 public class Singleton {//私有静态成员变量private static Singleton instance;//私有构造方法private Singleton(){}//公有静态访问方法在方法上加了一个synchronized关键字确保线程安全public static synchronized Singleton getInstance(){if(instance null)instance new Singleton();return instance;}}// 或者在类对象上加锁 public static Singleton getInstance() {synchronized(Singleton.class) { if (singleton null) {singleton new Singleton();}}return singleton;}
这样就规避了两个线程同时创建Singleton对象的风险但是引来另外一个问题每次去获取对象都需要先获取锁并发性能非常地差极端情况下可能会出现卡顿现象。 接下来要做的就是优化性能目标是如果没有实例化对象则加锁创建如果已经实例化了则不需要加锁直接获取实例
所以直接在方法上加锁的方式就被废掉了因为这种方式无论如何都需要先获取锁
接下来有下面的DCL
三、双重检测锁模式的懒汉式单例线程安全效率高 又叫DCL懒汉式 Double Check Lock public class Singleton {//私有静态成员变量加上了volatile关键字确保可见性private volatile static Singleton instance null;//私有构造方法private Singleton(){}//公有静态访问方法public static Singleton getInstance(){if(instance null){ //线程A和线程B同时看到singleton null如果不为null则直接返回singletonsynchronized (Singleton.class){ //线程A或线程B获得该锁进行初始化获取锁这里利用到volatile关键字的可见性再次判断空if(instance null) //其中一个线程进入该分支另外一个线程则不会进入该分支此时instance真的为空才去创建实例instance new Singleton();}}return instance;}}注意synchronized 解决并发问题但是因为lazyMan new LazyMan();不是原子性操作(可以分割见代码注释)可能发生指令重排序的问题通过volatil来解决
Java 语言提供了 volatile和 synchronized 两个关键字来保证线程之间操作的有序性volatile 是因为其本身包含“禁止指令重排序”的语义synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的此规则决定了持有同一个对象锁的两个同步块只能串行执行。原子性就是指该操作是不可再分的。不论是多核还是单核具有原子性的量同一时刻只能有一个线程来对它进行操作。简而言之在整个操作过程中不会被线程调度器中断的操作都可认为是原子性。比如 a 1 指令重排序是指JVM在保证最终结果正确的情况下可以不按照程序编码的顺序执行语句尽可能提高程序的性能 使用volatile关键字可以防止指令重排序其原理较为复杂这篇博客不打算展开可以这样理解使用volatile关键字修饰的变量可以保证其指令执行的顺序与程序指明的顺序一致不会发生顺序变换这样在多线程环境下就不会发生NPE异常了。volatile还有第二个作用使用volatile关键字修饰的变量可以保证其内存可见性即每一时刻线程读取到该变量的值都是内存中最新的那个值线程每次操作该变量都需要先读取该变量。 四、破坏懒汉式单例与饿汉式单例
无论是完美的懒汉式还是饿汉式终究敌不过反射和序列化它们俩都可以把单例对象破坏掉产生多个对象。
1.演示利用反射破坏单例模式
public static void main(String[] args) {// 获取类的显式构造器ConstructorSingleton construct Singleton.class.getDeclaredConstructor();// 可访问私有构造器construct.setAccessible(true); // 利用反射构造新对象Singleton obj1 construct.newInstance(); // 通过正常方式获取单例对象Singleton obj2 Singleton.getInstance(); System.out.println(obj1 obj2); // false
} 上述的代码一针见血了利用反射强制访问类的私有构造器去创建另一个对象 2.利用序列化与反序列化破坏单例模式
public static void main(String[] args) {// 创建输出流ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(Singleton.file));// 将单例对象写到文件中oos.writeObject(Singleton.getInstance());// 从文件中读取单例对象File file new File(Singleton.file);ObjectInputStream ois new ObjectInputStream(new FileInputStream(file));Singleton newInstance (Singleton) ois.readObject();// 判断是否是同一个对象System.out.println(newInstance Singleton.getInstance()); // false
} 两个对象地址不相等的原因是readObject() 方法读入对象时它必定会返回一个新的对象实例必然指向新的内存地址。
五、枚举实现
至此我们已经掌握了懒汉式与饿汉式的常见写法了在《大话设计模式》中的单例模式章节也止步于此。但是追求极致的我们怎么能够止步于此在《Effective Java》书中给出了终极解决方法话不多说学完下面真的不虚面试官考你了。
在 JDK1.5 后使用 Java 语言实现单例模式的方式又多了一种枚举
我们先来看看枚举如何实现单例模式的如下代码
public enum Singleton {INSTANCE;public void doSomething() {System.out.println(这是枚举类型的单例模式);}
}
需要思考使用枚举实现单例模式的优势在哪里
我们从最直观的地方入手第一眼看到这几行代码就会感觉到“少”没错就是少虽然这优势有些牵强但写的代码越少越不容易出错。
优势1代码对比饿汉式与懒汉式来说更加地简洁
其次既然是实现单例模式那这种写法必定满足单例模式的要求而且使用枚举实现时没有做任何额外的处理。
优势2它不需要做任何额外的操作去保证对象单一性与线程安全性
我写了一段测试代码放在下面这一段代码可以证明程序启动时仅会创建一个 Singleton 对象且是线程安全的。 我们可以简单地理解枚举实现单例的过程在程序启动时会调用Singleton的空参构造器实例化好一个Singleton对象赋给INSTANCE之后再也不会实例化 public enum Singleton {INSTANCE;Singleton() { System.out.println(枚举创建对象了); }public static void main(String[] args) { /* test(); */ }public void test() {Singleton t1 Singleton.INSTANCE;Singleton t2 Singleton.INSTANCE;System.out.print(t1和t2的地址是否相同 t1 t2);}
}
// 枚举创建对象了
// t1和t2的地址是否相同true
除了优势1和优势2还有最后一个优势让枚举实现单例模式在目前看来已经是“无懈可击”了。
优势3使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象破坏单例模式。
防破坏的原理如下
1防反射 枚举类默认继承了 Enum 类在利用反射调用 newInstance() 时会判断该类是否是一个枚举类如果是则抛出异常。
2防止反序列化创建多个枚举对象
在读入Singleton对象时每个枚举类型和枚举名字都是唯一的所以在序列化时仅仅只是对枚举的类型和变量名输出到文件中在读入文件反序列化成对象时利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。
所以在序列化和反序列化的过程中只是写出和读入了枚举类型和名字没有任何关于对象的操作。
小总结
1Enum 类内部使用Enum 类型判定防止通过反射创建多个对象
2Enum 类通过写出读入对象类型和枚举名字将对象序列化反序列化通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例防止通过反序列化构造多个对象
3枚举类不需要关注线程安全、破坏单例和性能问题因为其创建对象的时机与饿汉式单例有异曲同工之妙。
总结
1单例模式常见的写法有两种懒汉式、饿汉式
2饿汉式在类加载时已经创建好该单例对象在获取单例对象时直接返回对象即可不会存在并发安全和性能问题。
3懒汉式在需要用到对象时才实例化对象正确的实现方式是Double Check Lock解决了并发安全和性能低下问题
4在开发中如果对内存要求非常高那么使用懒汉式写法可以在特定时候才创建该对象
5如果对内存要求不高使用饿汉式写法因为简单不易出错且没有任何并发安全和性能问题
6为了防止多线程环境下因为指令重排序导致变量报NPE需要在单例对象上添加volatile关键字防止指令重排序
7最优雅的实现方式是使用枚举其代码精简没有线程安全问题且 Enum 类内部防止反射和反序列化时破坏单例。