单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
单例模式,顾名思义就是只有一个实例,并且她自己负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式的实现
- 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
- 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
单例模式的实现方法很多。但是,主要是下面的几种需要重点学习,入门掌握的。
第一种饿汉式
class Singleton {
private static Singleton instance = new Singleton();// 指向自己实例的私有静态引用,主动创建
private Singleton() {} // 私有的构造方法
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton getInstance() {
return instance;
}
}
分析优缺点:
优点:类加载的方式是按需加载,且加载一次。
在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
缺点:饿汉就是不管我用不用,都给你创建出来。从始至终从未使用过这个实例,则会造成内存的浪费。
第二种懒汉式
单线程版本
class Singleton {
private static Singleton instance = null;
private Singleton() {}
// 被动创建,在真正需要使用时才去创建
public static Singleton getInstance() {
if (instance == null) {//线程不安全
instance = new Singleton();
}
return instance;
}
}
单线程版本虽然可以延迟加载,避免不需要就被创建,但是当多个线程时候,不能保证单例模式,会重复多次new 对象,产生多个实例。
多线程版本
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
虽然保证了线程安全,但还是可能出现多次new 对象的情况。具体分体如下:
A线程和B线程都new的对象,不满足单例使用同一个对象的定义。
在这个过程中,B线程竞争对象锁失败,转变为阻塞态,等他再去执行竞争到对象锁的时候,不知道外面已经A线程已经new了(发生了天翻地覆的变化),于是又去执行new 操作。
- 单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
- 但是无法保证只new 一个对象,创建一个实例。
第三种 双重校验锁与volatile结合
未使用volatile关键字
public class Singleton{
private static Singleton instance;
//程序运行时创建一个静态只读的进程辅助对象
private static readonly object syncRoot = new object();
private Singleton() { }
public static Singleton GetInstance()
{
//先判断是否存在,不存在再加锁处理
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
return instance;
}
}
Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查, 这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
使用双重检测同步延迟加载去创建单例的做法不但保证了单例,而且切实提高了程序运行效率。
优点:线程安全;延迟加载;效率较高。
缺点:无法禁止指令重排序。 但是其实这个代码仔细看还是有问题的
给共享变量加上volatile关键字+ 双重校验锁
public class Singleton {
private Singleton() {} //私有构造函数
private volatile static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
分析为什么必须加上volatile关键字
这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到false。
但涉及到了JVM编译器的指令重排。
指令重排是什么意思呢? 比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
- memory =allocate(); //1:分配对象的内存空间
- ctorInstance(memory); //2:初始化对象
- instance =memory; //3:设置instance指向刚分配的内存地
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:- memory =allocate(); //1:分配对象的内存空间
- instance =memory; //3:设置instance指向刚分配的内存地址
- ctorInstance(memory); //2:初始化对象
当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance无效对象。如果之后主线程的代码 调用对象. xxx 使用对象就会出现报错:对象还没有初始化,无法使用。后续详细解释volatile关键字。
参考知乎回答
单例模式的使用场景
使用场景其实很多,由于入门所以暂时先写这两个。
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
volatile 关键字
作用
- 保证可见性。
- 不能解决原子性。
- 禁止指令重排序,建立内存屏障–参考单例模式说明
可见性
由于线程的工作内存是拷贝主内存的值,而一个变量如果在主内存里面更改,为了线程里面可以知道我这个共享变量已经更改了,不要在继续使用原来工作内存里面的那个变量值,所以加上了volatile 关键字。
任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于Valatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是有序的。
volatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说,因为volatile只能保证多线程的内存可见性,不能保证多线程的执行有序性。而最彻底的同步要保证有序性和可见性,例如synchronized。
原子性
count++ 和 new 操作都是非原子性的,依赖共享变量赋值不具有原子性。
以count++ 为例子:1.从主存读取值 (volatile 保证的是这个)2.修改 (这里保证不了)3.写回主存
new 一个对象:1. 分配内存空间 2 执行new 操作 3.赋值给变量
关于禁止重排序
如果volatile不禁止指令重排序,会出现什么问题
满足什么条件就可以重排序 :强调指令之间没有依赖关系。
关于new 操作由于分为三个步骤执行,重排序可以是
- 创建初始化内存空间
- 执行new 操作 (这一步是具体的在内存空间里面执行的)
- 赋值给变量 (让引用指向内存空间)
如果不禁止重排序,就会出现如果A线程执行了1-3 但还没来得及执行2,B线程判断发现引用指向不为空,就返回引用,之后B线程.调用对象操作,但是对象没有初始化完成。
关于new操作解释以及内存机制
volatile禁止指令重排序的作用:
被volatile修饰的个引用变量,分解后的指令如果中涉及到某一个步骤有对volatile修饰变量的操作,那么这个分解后的这个步骤禁止重排序。
即为第三步操作禁止重排序,第一步和第二步是不影响的。
使用场景
在上一篇博客中说到,如果某段代码涉及到读和写两种操作,但是都加上了synchronized 关键字,会大大降低性能。现在volatile在同时涉及到读写操作的时候就可以体现它的作用。
对于读写操作来说,读共享变量的操作保证可见性给共享变量加上volatile关键字。具体涉及到写操作就加上synchronized 。这样既能保证性能又能保证安全。