单例模式
一、为什么要使用单例模式
二、单例模式的分类
1、懒汉模式
懒汉模式是指当对象第一次被请求获取实例时,才进行对象的创建。
1)普通单例
public class Singleton {
//懒汉式--实际调用的时候才创建对象
private static Singleton lazyInstance;
//私有化构造函数,避免在外部被实例化。
private Singleton(){
}
//唯一对外暴露可以获取对象实例的方法
public synchronized static Singleton getLazyInstance(){
if(null == lazyInstance){
lazyInstance = new Singleton();
}
return lazyInstance;
}
}
这种是最简单的饿汉单例模式,但是有一个很大的缺陷就是每次想要获取lazyInstance都要竞争锁。
这样完全没有必要,因为lazyInstance在实例化后在JVM内存全局中只存在一个,任何方法调用都应该直接返回。反复竞争锁太消耗资源了。
为了解决普通单例锁竞争消耗资源的问题,我们一般采用DCL模式来获取单例。
2)DCL单例(double check lock)
public class Singleton {
//懒汉式--实际调用的时候才创建对象
//使用volatile关键字修饰是为了禁止指令重排序,以免线程获取到半初始化的对象
//例如:线程A在 lazyInstance = new Singleton(); 时发生了指令重排序,导致lazyInstance里面全是空值,而不是初始化后的值。
// 这时线程B调用getLazyInstance()方法,会把还没有完全初始化的lazyInstance取走,导致后续业务出现问题。
// 所以DCL创建单例,单例对象需要用volatile修饰。
private static volatile Singleton lazyInstance;
//私有化构造函数,避免在外部被实例化。
private Singleton(){
// 初始化逻辑...
// 初始化逻辑...
// 初始化逻辑...
}
public static Singleton getLazyInstance(){
//第一次判断,如果单例对象已经实例化,则直接返回,不需要再竞争锁
if(null ==lazyInstance){
//如果lazyInstance没有实例化,那么线程竞争锁进入实例化代码块
synchronized (Singleton.class) {
System.out.println(Thread.currentThread().getName()+"竞争到锁了!");
//第二次判断,所有竞争锁的线程都会尝试对lazyInstance进行实例化。
//所以需要第二次判断,只有第一个取到锁的线程才能进行实例化。
if(null == lazyInstance) {
System.out.println(Thread.currentThread().getName()+"进行了初始化!");
lazyInstance = new Singleton();
}else{
System.out.println(Thread.currentThread().getName()+"竞争到锁了,但是没有进行初始化。。。");
}
}
}
return lazyInstance;
}
}
测试代码:
public static void main(String[] args) {
int i = 0;
//1000个线程调用getLazyInstance()方法
while (i<1000){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Singleton.getLazyInstance());
}
}).start();
i++;
}
}
输出结果:
2、饿汉模式
1)普通饿汉式
public class HungrySingleton {
//普通饿汉式 类加载时就创建好对象
private static HungrySingleton hungryInstance = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getHungryInstance(){
return hungryInstance;
}
}
从上面代码可以看出,普通饿汉式就是在类加载阶段创建好对象。之后每次返回的都是一开始就创建好的对象,线程安全。
但是有个缺点就是如果单例对象比较多的话,会在类加载阶段消耗大量时间。
针对上述问题,可以采用内部类饿汉式加载。
2)内部类饿汉式实现懒加载
public class HungrySingleton {
//使用内部类进行饿汉式单例
private static class Singleton{
private static HungrySingleton instance = new HungrySingleton();
}
private HungrySingleton(){
}
//调用获取单例方法时,才加载内部类,凭此实现懒加载
public static HungrySingleton getHungryInstance(){
return Singleton.instance;
}
}
3、枚举实现单例
以上不管饿汉式还是懒汉式都有缺陷,就是可以破坏单例。
例如:
使用反射调用私有的构造方法。
使用序列化/反序列化创建对象。
如果要解决上述两个问题,可以采用枚举实现单例,这也是目前最被推荐的实现单例的方法。
//不实现接口也可以用枚举实现单例,这里只是按实现接口的方式写出而已
public enum EnumSingleton implements Resource {
SINGLETON{
@Override
public void doSomething() {
System.out.println("执行了接口方法!");
}
}
}
interface Resource {
void doSomething();
}
在我还没有了解枚举类型时,我不太明白枚举实现单例的应用场景是什么 其实现在也没太明白枚举 。
直到知道了枚举类型也可以继承,可以有自己的方法和内部变量。大体用法和普通java类类似。
我才明枚举实现单例和普通单例使用起来其实是一样的,只不过他有以下优点:
1、JVM会保证enum不能被反射并且构造器方法只执行一次。
2、此方法无偿提供了序列化机制,绝对防止反序列化时多次实例化。
3、运行时(compile-time )创建对象(懒加载)
和一个缺点:
不支持继承