单例模式在面试中比较常见的问题,大佬写的比较好,分享给大家:
单例模式
1.什么是单例模式?
单例模式是一种常用的软件设计模式,它定义是单例对象的类只能允许一个实例存在。确保一个类只有一个实例,并提供该实例的全局访问点。
该类负责创建自己的对象,同时确保只有一个对象被创建。一般常用在工具类的实现或创建对象需要消耗资源的业务场景。 单例模式的特点:
- 类构造器私有
- 持有自己类的引用 (自己创建自己唯一的实例)
- 对外提供获取实例的静态方法
类图: 举一个简单单例模式的例子
public class SimpleSingleton {
private static final SimpleSingleton INSTANCE=new SimpleSingleton();
private SimpleSingleton(){
}
public static SimpleSingleton getInstance(){
return INSTANCE;
}
}
做一个小测试,通过getInstance()获取两次示例,输出他们哈希值,看看结果是什么?
public class SimpleSingletonTest {
public static void main(String[] args) {
System.out.println(SimpleSingleton.getInstance().hashCode());
System.out.println(SimpleSingleton.getInstance().hashCode());
}
}
输出结果:
1554874502
1554874502
两次得到实例的哈希值相同,说明两个实例是同一个实例。
2.饿汉模式和懒汉模式
饿汉模式懒汉模式是实现单例模式常用的两种方式。
2.1.饿汉模式
实例在初始化的时候就已经建好了,之后就不会在实例化不管你有没有用到,先建好了再说。具体代码如下:
public class SimpleSingleton {
private static final SimpleSingleton INSTANCE = new SimpleSingleton();
private SimpleSingleton() {
}
public static SimpleSingleton getInstance() {
return INSTANCE;
}
}
还有另一种变种方式
public class SimpleSingleton {
private static final SimpleSingleton INSTANCE;
static{
INSTANCE=new SimpleSingleton();
}
private SimpleSingleton() {
}
public static SimpleSingleton getInstance() {
return INSTANCE;
}
}
由于饿汉模式只是最开始初始化的时候实例化,之后不会再被实例化,所有饿汉模式是线程安全的,但是也带来了缺点,一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?
2.2懒汉模式
顾名思义就是实例在用到的时候才去创建,需要用的时候才去检查有没有实例,如果有则返回,没有则新建。具体代码如下:
public class SimpleSingleton2 {
private static SimpleSingleton2 INSTANCE;
private SimpleSingleton2() {
}
public static SimpleSingleton2 getInstance() {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton2();
}
return INSTANCE;
}
}
懒汉模式虽然解决了资源浪费的问题,但是它缺带来了线程安全问题。 例如:有两个线程,他们同时调用getInstance() 方法,同时走到if (INSTANCE == null) ,同时判断INSTANCE == null成立, INSTANCE 会被实例化两次。这样就违背了单例模式的定义了。 解决办法: 利用synchronized 关键字修饰共有的静态方法getInstance() ,在getInstance 方法上加synchronized 关键字,对该方法加锁,保证在并发(多线程)的情况下,只有一个线程进入该方法创建INSTANCE 对象的实例。 代码:
public class SimpleSingleton2 {
private static SimpleSingleton2 INSTANCE;
private SimpleSingleton2() {
}
public static synchronized SimpleSingleton2 getInstance() {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton2();
}
return INSTANCE;
}
}
但是利用synchronized 关键字修饰公有的静态方法getInstance() 会降低getInstance() 方法的性能,因为,如果一个线程进入getInstance() 方法后,其他的线程必须等待。举个例子:如果INSTANCE 已经被实例化了,当一个在线程进入了getInstance() 方法,虽然此时INSTANCE!=null ,其他线程也需要等待。
3.双重检查锁
3.1.如何实现?
加锁操作只需要对实例化那部分的代码进行,只有当 INSTANCE 没有被实例化时,才需要进行加锁。双重校验锁先判断 INSTANCE 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁,加锁时候在检查一次INSTANCE 是否为空 代码:
public class SimpleSingleton4 {
private static SimpleSingleton4 INSTANCE;
private SimpleSingleton4() {
}
public static SimpleSingleton4 getInstance() {
if (INSTANCE == null) {
synchronized (SimpleSingleton4.class) {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton4();
}
}
}
return INSTANCE;
}
}
在加锁之前判断是否为空,可以确保INSTANCE 不为空的情况下,不用加锁,可以直接返回。 为什么在加锁之后,还需要判断INSTANCE 是否为空呢?
答:是为了防止在多线程并发的情况下,实例化多个对象。
**比如:线程a和线程b同时调用getInstance 方法,假如同时判断INSTANCE 都为空,这时会同时进行抢锁。假如线程a先抢到锁,开始执行synchronized关键字包含的代码,此时线程b处于等待状态。线程a创建完新实例了,释放锁了,此时线程b拿到锁,进入synchronized 关键字包含的代码,如果没有再判断一次INSTANCE 是否为空,则可能会重复创建实例。所以需要在synchronized 前后两次判断。
3.2.volatile关键字
我们写好的程序是这样的。
public static SimpleSingleton4 getInstance() {
if (INSTANCE == null) {
synchronized (SimpleSingleton4.class) {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton4();
}
}
}
return INSTANCE;
}
注意第4处, INSTANCE = new SimpleSingleton4( ),这条语句不具备原子性,new关键字在创建一个对象的示例时分为三步:
- 分配内存
- 调用构造函数,初始化
- 将对象引用赋值给变量(将对象的地址分配给
INSTANCE ) 在JVM中,java 虚拟机会对上述指令优化重排,可能改变部分指令的顺序,
上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
volatile 关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止JVM指令重排。
4.静态内部类
4.1.静态内部类如何实现单例
具体代码如下:
public class SimpleSingleton5 {
private SimpleSingleton5() {
}
public static SimpleSingleton5 getInstance() {
return Inner.INSTANCE;
}
private static class Inner {
private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
}
}
我们看到在SimpleSingleton5 类中定义了一个静态的内部类Inner 。在SimpleSingleton5类的getInstance 方法中,返回的是内部类Inner 的实例INSTANCE对象。 只有在程序第一次调用getInstance 方法时,虚拟机才加载Inner 并实例化INSTANCE 对象。 java内部机制保证了,只有一个线程可以获得对象锁,其他的线程必须等待,保证对象的唯一性。
4.2.反射漏洞
代码:
@Test
public void test1(){
Class<SimpleSingleton4> simpleSingleton4Class=SimpleSingleton4.class;
try {
Constructor<SimpleSingleton4> declaredConstructor = simpleSingleton4Class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
SimpleSingleton4 newInstance = declaredConstructor.newInstance();
System.out.println("newInstance == SimpleSingleton4.getInstance():"+(newInstance == SimpleSingleton4.getInstance()));
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
输出:
newInstance == SimpleSingleton4.getInstance():false
由此看出,通过反射创建的对象,跟通过getInstance方法获取的对象,并非同一个对象,也就是说,这个漏洞会导致SimpleSingleton4非单例。 那么,要如何防止这个漏洞呢? 这就需要在无参构造方式中判断,如果非空,则抛出异常了。
private SimpleSingleton4(){
if(Inner.INSTANCE != null) {
throw new RuntimeException("不能支持重复实例化");
}
}
4.3.序列化漏洞
众所周知,java中的类通过实现Serializable接口,可以实现序列化。 我们可以把类的对象先保存到内存,或者某个文件当中。后面在某个时刻,再恢复成原始对象。 但是在反序列化的时候会创建新的实例,打破了单例模式对象唯一的要求。
@Test
public void test2() throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(SimpleSingleton4.getInstance());
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
SimpleSingleton4 newInstance = (SimpleSingleton4) ois.readObject();
System.out.println("newInstance == SimpleSingleton4.getInstance():"+(newInstance==SimpleSingleton4.getInstance()));
}
输出:
newInstance == SimpleSingleton4.getInstance():false
那么,如何解决这个问题呢?
答:重新readResolve方法。
private Object readResolve() throws ObjectStreamException {
return Inner.INSTANCE;
}
再一次运行结果:
newInstance == SimpleSingleton4.getInstance():true
程序在反序列化获取对象时,会去寻找readResolve()方法。
- 如果该方法不存在,则直接返回新对象。
- 如果该方法存在,则按该方法的内容返回对象。
- 如果我们之前没有实例化单例对象,则会返回null。
5.枚举
其实在java中枚举就是天然的单例,每一个实例只有一个对象,这是java底层内部机制保证的。 在枚举对象唯一性的这个特性,还能创建其他的单例对象,例如:
package job.designpattern;
public enum SimpleSingleton5 {
INSTANCE;
private Student instance;
SimpleSingleton5(){
instance=new Student();
}
public Student getInstance(){
return instance;
}
}
class Student{
}
jvm保证了枚举是天然的单例,并且不存在线程安全问题,此外,还支持序列化
6.多例模式
单例模式,只会产生一个实例。但它其实还有一个变种。 多例模式,顾名思义,它允许创建多个实例。但它的初衷是为了控制实例的个数,其他的跟单例模式差不多。 具体实现代码如下:
public class SimpleMultiPattern {
private static final SimpleMultiPattern INSTANCE1 = new SimpleMultiPattern();
private static final SimpleMultiPattern INSTANCE2 = new SimpleMultiPattern();
private SimpleMultiPattern() {
}
public static SimpleMultiPattern getInstance(int type) {
if(type == 1) {
return INSTANCE1;
}
return INSTANCE2;
}
}
有些朋友可能会说:既然多例模式也是为了控制实例数量,那我们常见的池技术,比如:数据库连接池,是不是通过多例模式实现的? 答:不,它是通过享元模式实现的。 那么,多例模式和享元模式有什么区别? 多例模式:跟单例模式一样,纯粹是为了控制实例数量,使用这种模式的类,通常是作为程序某个模块的入口。 享元模式:它的侧重点是对象之间的衔接。它把动态的、会变化的状态剥离出来,共享不变的东西。
7.应用场景
7.1 .Runtime
jdk提供了Runtime类,我们可以通过这个类获取系统的运行状态。
7.2.LogFactory
mybatis提供LogFactory类是为了创建日志对象,根据引入的jar包,决定使用哪种方式打印日志.
7.3. spring的单例
以前在spring中要定义一个bean,需要在xml文件中做如下配置:
<bean id="test" class="com.susan.Test" init-method="init" scope="singleton">
在bean标签上有个scope属性,我们可以通过指定该属性控制bean实例是单例的,还是多例的。如果值为singleton,代表是单例的。当然如果该参数不指定,默认也是单例的。如果值为prototype,则代表是多例的。
|