我向面试官讲解了单例模式,他对我竖起了大拇指
什么是单例模式
单例模式的类型
懒汉式
:在真正需要使用对象时才去创建该单例类对象饿汉式
:在类加载时已经创建好该单例对象,等待被程序使用
懒汉式创建单例对象
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
饿汉式创建单例对象
类加载
时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。public class Singleton{
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return singleton;
}
}
懒汉式如何保证只创建一个对象
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
线程安全
问题。public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
// 或者
public static Singleton getInstance() {
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
优化性能
:目标是如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
第 2 行代码,如果 singleton 不为空,则直接返回对象,不需要获取锁;而如果多个线程发现 singleton 为空,则进入分支; 第 3 行代码,多个线程尝试争抢同一个锁,只有一个线程争抢成功,第一个获取到锁的线程会再次判断singleton 是否为空,因为 singleton 有可能已经被之前的线程实例化 其它之后获取到锁的线程在执行到第 4 行校验代码,发现 singleton 已经不为空了,则不会再 new 一个对象,直接返回对象即可 之后所有进入该方法的线程都不会去获取锁,在第一次判断 singleton 对象时已经不为空了
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
}
使用 volatile 防止指令重排
volatile 还有第二个作用:使用 volatile 关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
public class Singleton {
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
}
破坏懒汉式单例与饿汉式单例
利用反射破坏单例模式
public static void main(String[] args) {
// 获取类的显式构造器
Constructorconstruct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton obj1 = construct.newInstance();
// 通过正常方式获取单例对象
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
利用序列化与反序列化破坏单例模式
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
}
让面试官鼓掌的枚举实现
枚举
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("这是枚举类型的单例模式!");
}
}
优势 1 :一目了然的代码
public enum Test {
INSTANCE;
}
少
,没错,就是少,虽然这优势有些牵强,但写的代码越少,越不容易出错。优势 2:天然的线程安全与单一实例
我们可以简单地理解枚举创建实例的过程:在程序启动时,会调用 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
保护单例模式
,它使得枚举在当前的单例模式领域已经是 无懈可击
了优势 3:枚举保护单例模式不被破坏
总结
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️