单例模式-上

单例模式是什么?

单例模式看似是设计模式中最简单的一个类,并且其类图中只有一个类。它的主要原则是该类负责创建自己的对象,并且只有单个对象被创建。

单例模式的用处?

对于那些只需要一个的对象,比如:线程池 (threadpool) 、缓存 (cache) 、注册表 (registry)的对象。

单例模式的四种实现方法

经典的单例模式

实现代码

public class Singleton {
    private static Singleton instance;
	//构造函数为private, 防止该类的实例被错误的创建
    private Singleton() {
    }

    public static Singleton getInstance() {
        //保证实例只会被创建一次
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

问题

当多个线程执行时会出现不符合预期的结果

// 实现Runnable接口
public class T implements Runnable{
    @Override
    public void run(){
        Singleton instance = Singleton.getInstance();
        System.out.println(Thread.currentThread().getName()+"  "+instance);
    }
}
// 测试类
public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
    }
}

运行结果:

结果分析:

    public static Singleton getInstance() {
        //保证实例只会被创建一次
        [0]if (instance == null) {
            [1]instance = new Singleton();
        }
        [2]return instance;
    }

假设两个线程分别叫t0、t1,若通过调试工具手动干预使其执行顺序为:

t0[0]->t1[0]->t0[1]->t0[2]->t1[1]->t1[2]

则会导致出现了两个实例,不符合单例模式的要求

加锁的单例模式

实现代码

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    // 使用synchronized进行同步,对Singleton类加锁
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

问题

虽然通过同步的方法解决了多线程的问题,但是同步影响了执行的性能。并且实例只需要创建一次,但是通过该方法每次调用getInstance() 函数都需要同步,很累赘。

急切的单例模式

实现代码

public class Singleton {
    // 利用静态变量,实现单例
    private static final Singleton instance = new Singleton();

    //* 虽然不调用构造函数,需要防止系统自动创建public的构造函数
    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

问题

该方案很好的解决多线程带来的问题,并且未使用同步方法。当类加载的时候便创建了其实例,没有延迟加载,可能导致内存的浪费。

扫描二维码关注公众号,回复: 10389691 查看本文章

双重检查加锁

实现代码

public class Singleton {
    // 实现细节1:volatile关键字
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                // 实现细节2:两次判断是否为null
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

实现细节

实现细节1:为什么要加volatile关键字?
instance = new Singleton()	

因为上面这句代码并非原子操作,实际经历了三个步骤

  1. 给对象分配内存
  2. 初始化对象
  3. 设置instance引用指向刚刚分配的内存地址

若不使用volatile关键词,JVM重排序可能导致其程序执行顺序为1->3->2

假设有线程t0、t1,可能出现如下情况:

  1. t0 给对象分配内存
  2. t0 设置instance引用指向刚刚分配的内存地址[此时instance != null]
  3. t1 判断instance是否为null
  4. t1 初次访问对象
  5. t0 初始化对象
  6. t0 初次访问对象

使用volatile关键字修饰的共享变量使用缓存一致性协议,保证了内存的可见性

实现细节2:为什么要使用两次检查

若不使用两次检查,假设有线程t0、t1,可能出现如下情况:

  1. t0 判断instance是否为null
  2. t1 判断instance是否为null
  3. t0 进入临界区创建对象实例
  4. t0 返回
  5. t1 进入临界区创建对象实例
  6. t1 返回

此时t0和t1创建了两个不同的实例,违反了单例的原则

问题

代码复杂,在JDK1.4以及更早版本的Java中,volatile关键字的实现会导致双重检查加锁的失效

内部类

public class Singleton {
    
    private Singleton() {
    }

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

通过该方法可以很好的多线程的问题,并且由于不需要锁,在高并发环境下具有优越的性能。并且其不同于类的静态变量的实现方法,仅在第一次调用getInstance()函数的时候才会创建实例,因此不会出现内存的浪费的情况。

普遍的问题

上述实现看似很完美,但是仍有一些问题

  1. 序列化破坏单例模式
  2. 反射攻击破坏单例模式

猜你喜欢

转载自www.cnblogs.com/YuanJieHe/p/12616921.html