单例模式
单例模式(Singleton)是一种常用的软件设计模式,属于创建类型的一种。它确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。单例模式的主要目标是提供一个全局的访问点。
什么是设计模式?
设计模式就好比象棋,围棋中的"棋谱"。在"棋谱"中,大佬们把常见的对局情景给推演出来了,针对红方的一些走法,黑方应招的时候有 一些固定的套路,按照套路来走局势就不会吃亏。
软件开发中也有很多常见的 "问题场景",针对这些问题场景,,大佬们总结出了一些固定的套路, 按照这个套路来实现代码,也不会吃亏。
单例模式的两种实现方式
单例模式具体的实现方式, 分成 "饿汉" 和 "懒汉" 两种。
饿汉模式
饿汉模式是在类加载过程中就把实例创建出来了。之所以叫"饿汉",因为饿了很久的人看到食物就会非常急切,而这种在类加载过程中就把实例创建出来了就给人一种非常"急切"的感觉。
我们就用代码来描述一下饿汉:
class Singleton{
// 在此处, 先把这个实例给创建出来了.
private static Singleton instance = new Singleton();
//getInstance()方法用于获取Singleton类的唯一实例
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
但是这又是如何保证实例唯一呢?
不妨写下Main方法,在Main方法中这样是会报错。
public class Test {
public static void main(String[] args) {
Singleton s =new Singleton();
System.out.println(s1 == s2);
}
}
因为前面我们已经将构造方法设为private,这样在类外就无法通过 new 的方式来创建这个 Singleton 实例。我们通过调用getInstance方法就能获取实例。
public class Test {
public static void main(String[] args) {
//Singleton s =new Singleton();
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
这样就能保证实例唯一。
完整运行一下结果
最后完整代码:
class Singleton{
// 在此处, 先把这个实例给创建出来了.
private static Singleton instance = new Singleton();
//getInstance()方法用于获取Singleton类的唯一实例
public static Singleton getInstance(){
return instance;
}
// 为了避免 Singleton 类不小心被复制出多份来.
// 把构造方法设为 private. 在类外面, 就无法通过 new 的方式来创建这个 Singleton 实例了!!
private Singleton(){
}
}
public class Test {
public static void main(String[] args) {
//Singleton s =new Singleton(); 在类外面, 就无法通过 new 的方式来创建这个 Singleton 实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
懒汉模式
懒汉模式是在类加载的时候不会创建实例,而是第一次使用时才会创建实例。
我们还是用代码来描述一下懒汉:
class SingletonLazy {
volatile private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
再写下Main方法来试试:
public class Test {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1==s2);
}
}
最后运行结果为
完整代码如下:
class SingletonLazy {
volatile private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
public class Test {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1==s2);
}
}
饿汉模式和懒汉模式中的线程安全问题
上面描述的饿汉模式与懒汉模式如果是在多线程情况下调用getInstance方法,是否会出现线程安全问题呢?
在饿汉模式中,在类加载时就完成了初始化,getInstance直接返回instance实例,这个操作本质上就是"读操作",那么多个线程读取同一个变量是线程安全的。
new操作的本质也是多个指令。在懒汉模式中,t1线程先行判断instance是否为null,随后切换到t2线程,也进行了一次判断,且new了一个新的实例出来,最后继续执行t1线程,由于之前判断的条件仍然满足,所以这里同样会new一个新的实例。这样就"既有读又有写",这样就会出现线程安全问题。
如何让懒汉模式 能够成为线程安全的呢?
没错!!加锁
此时已经使用synchronized对new操作加锁
这段代码中还有一个问题:加锁操作是有开销的,我们真的需要每次都加锁吗?
这里的加锁只是在new出对象之前加锁,这是有必要的。但是一旦new完对象,后续调用getInstance,此时的instance值是非空的,因此会触发return。这俩操作都相当于读操作,不加锁也没关系。
所以,如果对象还没创建才加锁,如果对象已经创建就不用加锁了。
但是这个代码还有问题!
内存可见性问题 与 指令重排序问题
内存可见性问题:假设多个线程都去进行getIntance,这个时候就会有被优化的风险。
指令重排序问题:
instance = new SingletonLazy();
可以拆分为三个步骤:
1.申请内存空间
2.调用构造方法,把这个内存空间初始化成一个合理的对象
3.把内存空间的地址赋值给instance引用
正常情况下,按照1 2 3顺序来执行
但是编译器为了提高程序效率,会发生指令重排序,即调整代码的执行顺序,1 2 3这个顺序可能会变成 2 1 3
所以!! 我们要使用volatile 这个关键字来 解决这两个问题。
这样懒汉模式的线程安全问题就解决了。
以下是完整代码:
package Thread;
//懒汉模式
class SingletonLazy {
volatile private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
//使用双重 if 判定, 降低锁竞争的频率.
//给 instance 加上了 volatile.
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Test {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1==s2);
}
}