最近看了很多的书还有视频,他们都花了很长的篇幅提到了单例模式,于是我想把他们都总结起来,写下这篇文章。目的就是,让小白能搞懂单例模式,以及单例模式的经典面试题。为什么说是小白也能懂的呢?哈哈哈,还不是小胖也是一个小白~~~
单例模式定义:一个类只能有一个实例,且该类能自行创建这个实例的一种模式。其实单例模式在C#或者.NET里面更好理解,像win7的任务管理器,在系统中只能创建一个。有些理解了嘛?
单例模式只能有一个实例,实例化其实就是new的过程,是不可能阻止他人不去用new的。所以我们完全可以直接就把这个类的构造方法改成私有的。对于外部的代码,不能用new来实例化他,我们完全可以再写一个public方法,叫做getInstance(),这个方法的目的就是返回一个实例,但是在这个方法中,我们需要是否实例化的判断。
来一个简单的例子
package singleton;
/**
* 描述: 懒汉式(线程不安全)
* **/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3(){
}
//如果两个线程同时到达,会出现线程不安全的情况
public static Singleton3 getInstance(){
if (instance == null){
instance = new Singleton3();
}
return instance;
}
}
单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。可以节省内存和计算、保证结果正确、方便管理。适用场景是无状态的工具类、全局信息类。
记住,上面的单例模式是线程不安全的。
package singleton;
/***
* 描述:饿汉式(静态常量) (可用)
* **/
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
public static Singleton1 getInstance(){
return INSTANCE;
}
}
上面是饿汉式的静态常量的写法,可以看到类在加载后就完成了实例化的创建。优点:写法简单,类加载后就完成了实例的创建。缺点:提前占用系统的资源。
package singleton;
/***
* 描述:饿汉式(静态代码块) (可用)
* **/
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2(){
}
public static Singleton2 getInstance(){
return INSTANCE;
}
}
上面是饿汉式的静态代码块的方式,只不过和第一种有一些区别。优缺点和第一种是一样的。优点:写法简单,类加载后就完成了实例的创建。缺点:提前占用系统的资源。
package singleton;
/**
* 描述: 懒汉式(线程不安全)
* **/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3(){
}
//如果两个线程同时到达,会出现线程不安全的情况
public static Singleton3 getInstance(){
if (instance == null){
instance = new Singleton3();
}
return instance;
}
}
上面的懒汉式的写法,它是线程不安全的,因为在多线程的时候,可能会有两个线程同时到达instance==null,然后都会初始化,这就创建了两个对象了,是线程不安全的,不能使用。
package singleton;
/**
* 描述:懒汉式(线程安全)(不推荐)
* */
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
//但是效率不高
public synchronized static Singleton4 getInstance(){
if (instance == null){
instance = new Singleton4();
}
return instance;
}
}
上面是懒汉式的第二种写法,这种方法是线程安全的,但是不推荐使用,因为效率是低下的。在getInstance上面加上了synchronized的同步方法,那么只能有一个线程可以进入到这个方法中,但是在多线程的时候效率是非常低的,因为任何一个线程进入的这个方法,都需要去等待锁的释放,所以不推荐使用。
package singleton;
/**
* 描述:懒汉式(线程不安全) (不推荐)
* */
public class Singleton5 {
private static Singleton5 instance;
private Singleton5(){
}
public static Singleton5 getInstance(){
if (instance == null){
synchronized (Singleton5.class){
instance = new Singleton5();
}
}
return instance;
}
}
上面是懒汉式的第三种写法,是对第二种的改进,不在锁在方法上,加在创建对象上面,但是这可能会引发线程安全的问题。如果连个线程都判断instance==null,都进入到if里面,这时只有一个线程会运行,但是第一个线程执行完成之后,第二个线程还是会创建实例,那么就是线程不安全的。
package singleton;
/**
* 描述: 双重检查
* 优点: 线程安全;延迟加载;效率较高
* 为什么要double-check
* 1.线程安全
* 2.单check为什么不行?
* 3.放在判断后面会引发线程安全问题
* 4.单层锁,但是synchronized放在方法上,这样可以,但是会导致性能问题
*
* 为什么要用volatile
* 1.新建对象实际上有3个步骤(分配内存资源,调用构造函数,将对象指向分配的内存空间)新建对象不是原子操作。
* 2.JVM重排序会带来NPE(空指针的问题)
* 3.防止重排序
* */
public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6(){
}
public static Singleton6 getInstance(){
if (instance == null){
synchronized (Singleton6.class){
if (instance == null){
instance = new Singleton6();
}
}
}
return instance;
}
}
上面就是很有名的double-check单例模式了,它的优点有:线程安全;延迟加载;效率较高。它是线程安全的,而且效率很高,推荐我们在面试的时候使用。这个代码同时也引出了我们在面试过程中的2个问题。
懒汉式单例模式为什么要用double-check,不用就不安全吗?懒汉式单例模式为什么双重检查模式要用volatile?
package singleton;
/**
* 描述: 静态内部类方式,可用
* 懒汉
* ***/
public class Singleton7 {
private Singleton7(){
}
private static class SingletonInstance{
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance(){
return SingletonInstance.INSTANCE;
}
}
上面是静态内部类的方式,是可以推荐使用的,而且效率也是可以的。外部类加载,JVM不会创建多个实例。
package singleton;
/**
* 描述:单例模式:枚举 推荐用
* */
public enum Singleton8 {
INSTANCE;
public void whatever(){
//无论什么方法
}
}
上面是枚举方式的单例模式,是生产实践中最佳的单例模式的写法,同时可以防止反序列化破坏单例。
什么叫单例设计模式
答:单例模式的重点在于整个系统上共享一些创建时较耗资源的对象。整个应用只维护一个特定类实例,它被所有组件共同使用。Java.lang.Runtime是单例模式的经典例子。
你知道饿汉式的缺点吗?
答:饿汉模式,类一加载的时候就会实例化对象,所以要提前占用系统资源。
那懒汉式的缺点呢?
答:不会出现占用资源的问题,但是需要使用合适,否则会带来线程安全问题。
懒汉式单例模式为什么要用double-check,不用就不安全吗?
答:为了线程安全,我们需要使用double-check。
追问,单check为什么不行?(代码见5.懒汉式)
答:单check是线程不安全的(代码见5.懒汉式),可能会有多个线程走到了 if (instance == null)的里面,由于synchronized看似只能有一个线程会创建对象,但是第二个也会创建。
追问,你可以把synchronized写在方法的外面呀?(代码见4,。懒汉式)
答:这个是可以解决线程安全的问题,但是效率不是很高,每个线程都需要等待锁的释放,会导致性能的问题,不推荐使用。
你说为什么要用volatile?
答:在多线程的时候,创建对象分为3步,CPU可能会重排序,首先建一个空的对象,然后复制给引用,然后调用构造方法。
第一个线程进来了,第一个对象已经不是空的,但是构造方法没有执行,里面的属性是空的。第二个线程发现不是空的,就会直接跳过创建实例的方法,之后再使用的时候引发的问题。使用volatile可以避免这个问题,对于第二个线程来说,他的创建过程对第一个线程来说是可见了,他就会等待创建完成。
那这么多应该如何选择,用哪种单例的实现方案最好?
答:枚举方式的单例模式,是生产实践中最佳的单例模式的写法,同时可以防止反序列化破坏单例。
那你知道happens-before原则嘛?
答:volatile就是happens-before原则呀…未完待续
请用Java写出线程安全的单例模式。
答:上面已经很多例子了,小胖觉得可以用第6种double-check。
《剑指offer》上面的推荐给面试官的解法是1.饿汉式(静态常量)(可用)和7.懒汉(静态内部类方式)(可用)。
《大话设计模式》上面的推荐也是1.饿汉式(静态常量)(可用)
《线程八大核心+Java并发底层原理精讲》上面推荐使用6.懒汉式(双重检查)(推荐面试使用)
小胖觉得可以使用第6种,这可能会打开一些面试的问题,把问题引入到我们了解熟悉的方向。当然你在手写单例模式的时候,可以去询问一下要求,是需要饿汉式还是懒汉式。
书籍1:《大话设计模式》 第21章 有些类也需计划生育–单例模式
书籍2:《剑指offer》 面试题2:实现Singleton模式
视频:《线程八大核心+Java并发底层原理精讲》
本系列想制作23种设计模式+7种设计原则一系列课程,其目的就是一个简单的记录学习的过程。不知道能帮助到多少人,也不知道技术是否会有一定的深度。
制作不易,您的点赞是我最大的动力。希望我们都能成为想成为的人
希望我们都能成为想成为的人