一、概念
1.单例设计模式概述
单例模式就是要确保类在内存中只有一个对象,该实例必须自动创建,并且对外提供。
2.优点
在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
3.缺点
没有抽象层,因此扩展很难。
职责过重,在一定程序上违背了单一职责
单例模式分饿汉模式和懒汉模式
二、饿汉模式
对象的创建随着类的加载而创建
/**
* 单例模式-饿汉模式
* @author guxl
*/
public class Student {
/**
* step1:把构造方法私有化,禁止类用户创建此类对象
*/
private Student() {}
/**
* step2:在成员位置自己创建一个私有静态对象
*/
private static Student student = new Student();
/**
* step3:通过一个静态公共的方法访问
*/
public static Student getStudent() {
return student;
}
}
JDK Runtime类 就是用的饿汉模式
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
三、懒汉模式
对象在被使用的时候才去创建
不完整的代码示例(注意这个示例,会出现线程安全问题)
public class Teacher {
//构造方法私有化
private Teacher(){}
//在成员位置自己创建一个私有静态对象
public static Teacher teacher = null;
//通过一个静态公共的方法访问
public static Teacher getTeacher(){
if(teacher == null){
return new Teacher();
}
return teacher;
}
}
但是上述懒汉模式是有线程安全问题的
线程安全问题的三个条件
a:是否多线程环境
b:是否有共享数据
c:是否有多条语句操作共享数据
如果在多线程环境下,在getTeacher()的时候会发生线程安全问题。这时候使用加锁来完善
如果锁加在了方法或者进入方法直接加锁太影响效率问题,所以采取锁加在new Teacher()这一块
public static Teacher getTeacher(){
if(teacher == null){
synchronized (Teacher.class){
return new Teacher();
}
}
return teacher;
}
但是还是有问题
虽然加锁了,如果有两个线程同时进入if条件内,第一个线程拿到锁然后去new,第二个线程在此等锁。等第二个线程拿到锁就会第二次执行new,这不符合单例模式的设计。
所以采取DCL模式,在锁内再次判断teacher == null
public static Teacher getTeacher(){
if(teacher == null){
synchronized (Teacher.class){
if(teacher == null){
return new Teacher();
}
}
}
return teacher;
}
从代码里可以看到,做了两重的singleton == null的判断,中间还用了synchronized关键字。
第一个singleton == null的是为了避免线程串行化,如果为空,就进入synchronized代码块中,获取锁后再操作,如果不为空,直接就返回singleton对象了,无需再进行锁竞争和等待了。
第二个singleton == null的是为了防止有多个线程同时通过第一个singleton == null,比如线程一先获取到锁,进入同步代码块中,发现singleton实例还是null,就会做new操作,然后退出同步代码块并释放锁,这时一起跳过第一层singleton == null的判断的还有线程二,这时线程一释放了锁,线程二就会获取到锁,如果没有第二层的singleton == null这个判断挡着,那就会再创建一个singleton实例,就违反了单例的约束了。
但是上述代码还是有问题,因为JVM会为了优化进行指令重排序。
首先明确一下JVM对new的指令操作,比如Teacher teacher = new Teacher()这段代码其实不是原子性的操作,它至少分为以下3个步骤:
- 给Teacher对象分配内存空间(对象分配内存)
- 调用Teacher类的构造函数等,初始化teacher对象(内存空间初始化)
- 将teacher引用指向分配的内存空间,这步一旦执行了,那teacher引用就不等于null了(修改引用指向那块内存)
JVM会为了优化,而做指令重排序的操作,这里的指令,指的是CPU层面的。
正常情况下,Teacher teacher = new Teacher()的步骤是按照1->2->3这种步骤进行的,但是一旦JVM做了指令重排序,那么顺序很可能编程1->3->2,即先指向内存,后初始化。如果是这种顺序,可以发现,在3步骤执行完singleton引用就不等于null,但是它其实还没做步骤二的初始化工作,但是另一个线程进来时发现,singleton不等于null了,就这样把半成品的实例返回去,调用是会报错的。
可以画个出现指令重排序后(1->3->2)的图加深下理解:
出现了指令重排序后,按照上图的流程逻辑,很可能会返回还没完成初始化的teacher对象,导致使用这个对象时报错,
volatile关键字--禁止指令重排序
所以现在要使用volatile关键字,volatile关键字的作用之一就是禁止指令重排序。
懒汉模式完整示例
/**
* 单例模式-懒汉模式
* @author guxl
*/
public class Teacher {
/**
* 构造方法私有化
*/
private Teacher(){}
/**
* 在成员位置自己创建一个私有静态对象
* 加上volatile关键字是为了防止指定重排序
*/
public static volatile Teacher teacher;
/**
* 通过一个静态公共的方法访问
* @return
*/
public static Teacher getTeacher(){
if(teacher == null){
synchronized (Teacher.class){
if(teacher == null){
return new Teacher();
}
}
}
return teacher;
}
}