单例模式是一种常用的软件设计模式,它定义是单例对象的类只能允许一个实例存在。确保一个类只有一个实例,并提供该实例的全局访问点。
该类负责创建自己的对象,同时确保只有一个对象被创建。一般常用在工具类的实现或创建对象需要消耗资源的业务场景。
单例模式的特点:
类图:
举一个简单单例模式的例子
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
两次得到实例的哈希值相同,说明两个实例是同一个实例。
饿汉模式懒汉模式是实现单例模式常用的两种方式。
实例在初始化的时候就已经建好了,之后就不会在实例化不管你有没有用到,先建好了再说。具体代码如下:
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; } }
由于饿汉模式只是最开始初始化的时候实例化,之后不会再被实例化,所有饿汉模式是线程安全的,但是也带来了缺点,一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?
顾名思义就是实例在用到的时候才去创建,需要用的时候才去检查有没有实例,如果有则返回,没有则新建。具体代码如下:
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
,其他线程也需要等待。
加锁操作只需要对实例化那部分的代码进行,只有当 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
前后两次判断。
我们写好的程序是这样的。
public static SimpleSingleton4 getInstance() { if (INSTANCE == null) {//1 synchronized (SimpleSingleton4.class) {//2 if (INSTANCE == null) {//3 INSTANCE = new SimpleSingleton4();//4 } } } return INSTANCE;//5 }
注意第4处, INSTANCE = new SimpleSingleton4(
),这条语句不具备原子性,new关键字在创建一个对象的示例时分为三步:
INSTANCE
)上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
volatile
关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止JVM指令重排。
具体代码如下:
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内部机制保证了,只有一个线程可以获得对象锁,其他的线程必须等待,保证对象的唯一性。
代码:
@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("不能支持重复实例化"); } }
众所周知,java中的类通过实现Serializable接口,可以实现序列化。
我们可以把类的对象先保存到内存,或者某个文件当中。后面在某个时刻,再恢复成原始对象。
但是在反序列化的时候会创建新的实例,打破了单例模式对象唯一的要求。
@Test public void test2() throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(SimpleSingleton4.getInstance()); //Read Obj from file 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()方法。
其实在java中枚举就是天然的单例,每一个实例只有一个对象,这是java底层内部机制保证的。
在枚举对象唯一性的这个特性,还能创建其他的单例对象,例如:
package job.designpattern; public enum SimpleSingleton5 { INSTANCE; private Student instance; SimpleSingleton5(){ instance=new Student(); } public Student getInstance(){ return instance; } } class Student{ }
jvm保证了枚举是天然的单例,并且不存在线程安全问题,此外,还支持序列化
单例模式,只会产生一个实例。但它其实还有一个变种。
多例模式,顾名思义,它允许创建多个实例。但它的初衷是为了控制实例的个数,其他的跟单例模式差不多。
具体实现代码如下:
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; } }
有些朋友可能会说:既然多例模式也是为了控制实例数量,那我们常见的池技术,比如:数据库连接池,是不是通过多例模式实现的?
答:不,它是通过享元模式实现的。
那么,多例模式和享元模式有什么区别?
多例模式:跟单例模式一样,纯粹是为了控制实例数量,使用这种模式的类,通常是作为程序某个模块的入口。
享元模式:它的侧重点是对象之间的衔接。它把动态的、会变化的状态剥离出来,共享不变的东西。
jdk提供了Runtime类,我们可以通过这个类获取系统的运行状态。
mybatis提供LogFactory类是为了创建日志对象,根据引入的jar包,决定使用哪种方式打印日志.
以前在spring中要定义一个bean,需要在xml文件中做如下配置:
<bean id="test" class="com.susan.Test" init-method="init" scope="singleton">
在bean标签上有个scope属性,我们可以通过指定该属性控制bean实例是单例的,还是多例的。如果值为singleton,代表是单例的。当然如果该参数不指定,默认也是单例的。如果值为prototype,则代表是多例的。