什么是单例模式
保证一个类只创建一个实例,并提供一个访问它的全局访问点。
特点:
1、这个类只能有一个实例
2、必须自行创建这个实例
3、它必须自行向系统提供这个实例
饿汉式
public final class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
特点
优点: 线程安全 static修饰的成员变量会在类初始化的过程中被收集进入类构造器方法中, 在多线程的场景下,JVM会保证只有一个线程能执行该类的方法,其他线程将会被阻塞等待,等到唯一的方法执行完成,其他线程将不会再执行方法,转而执行自己的代码,因此static修饰的成员变量在多线程下能保证只实例化一次。
缺点:这种模式在类的初始化阶段就开辟出内存空间,在成员变量较多或者较大的情况下,当没有使用当前对象时会一直占用堆内存
懒汉式
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (null == instance) {
instance = new Singleton2();
}
return instance;
}
}
特点
模式使用懒加载方式,由于对象被使用到的时候才会将实例加载到堆内存中,避免初始化时占用内存空间,但是它存在线程不安全的问题
public class TestDemo {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
T3 t3 = new T3();
T4 t4 = new T4();
T5 t5 = new T5();
T6 t6 = new T6();
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}
class T1 extends Thread{
@Override
public void run() {
System.out.println(Singleton2.getInstance());
}
}
class T2 extends Thread{
@Override
public void run() {
System.out.println(Singleton2.getInstance());
}
}
class T3 extends Thread{
@Override
public void run() {
System.out.println(Singleton2.getInstance());
}
}
class T4 extends Thread{
@Override
public void run() {
System.out.println(Singleton2.getInstance());
}
}
class T5 extends Thread{
@Override
public void run() {
System.out.println(Singleton2.getInstance());
}
}
class T6 extends Thread{
@Override
public void run() {
System.out.println(Singleton2.getInstance());
}
}
运行上述代码以后6个线程出现了两个不同地址的实例,这就违反了单例模式的初衷。
那么如何解决这问题呢,我们都知道多线程出现资源竞争都是通过加锁来解决,那么如何加锁,怎么样去加合适呢。
在getInstance方法上加锁,因为同步锁会带来锁竞争,带来系统性能开销,从而导致系统性能下降,并且每次请求获取类对象的时候,都会通过 getInstance() 方法获取,除了第一次为 null,其它每次请求基本都是不为 null 的。在没有加同步锁之前,是因为 if 判断条件为 null 时,才导致创建了多个实例。为了避免这一点我们可以在if判断中加锁,减小锁的范围
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (null == instance) {
synchronized (Singleton2.class) {
//只有当instance为null时才加锁,减少系统的资源开销
instance = new Singleton2();
}
}
return instance;
}
}
线程测试,这样做还是会存在线程安全的问题,并不能真正实现单例模式的初衷。因为在多线程的场景下,进入判断条件的线程,还是会获取锁创建实例,然后释放锁,在这个过程中有其他的线程进入判断以后,还是会创建实例。此时在进入判断以后再加入一个判断,判断当前实例是否为null,这样能有效避免创建多个实例了。
这种方式通常被称为Double-Check,但是这里还会有其他的问题,因为现在的虚拟机编译器为了尽可能的减少寄存器的读取、存储次数,会充分复用存储的的存储值,如下代码
- 步骤1:int a = 1;//步骤1:加载a变量的内存地址到寄存器中,加载1到寄存器中,CPU通过mov指令把1写入到寄存器指定的内存中
- 步骤2:int b = 2;//步骤2 加载b变量的内存地址到寄存器中,加载2到寄存器中,CPU通过mov指令把2写入到寄存器指定的内存中
- 步骤3:a = a + 1;//步骤3 重新加载a变量的内存地址到寄存器中,加载1到寄存器中,CPU通过mov指令把1写入到寄存器指定的内存中
正常的执行顺序是1-2-3,可是jvm可以对它们进行任意排序以提高性能,这也是虚拟机的编译重排序(这个后面学习到在讲)这样一来Double-Check模式下,假设单例类中的存在其他的属性,并且其他的属性也需要实例化,这个时候除了要实例化单例本身,还要对其他属性进行实例化
//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton2 {
private static Singleton2 instance= null;//不实例化
public List<String> list = null;//list属性
private Singleton2(){
list = new ArrayList<String>();
}//构造函数
public static Singleton2 getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//第一次判断,当instance为null时,则实例化对象,否则直接返回对象
synchronized (Singleton2.class){//同步锁
if(null == instance){//第二次判断
instance = new Singleton2();//实例化对象
}
}
}
return instance;//返回已存在的对象
}
}
在执行 instance = new Singleton2(); 代码时,正常情况下,实例过程这样的:
- 给 Singleton 分配内存;
- 调用 Singleton 的构造函数来初始化成员变量;
- 将 Singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)。
如果虚拟机发生了重排序,这个时候步骤3可能在步骤2之前完成,此时刚好有一个线程到了第一次判断,此时instance为非null,并返回使用,而实际上给对象并未完成实例化,这样会导致系统出错。此时Happens-Before规则就发生了效果。
Happens-Before定义
前一个操作的结果可以被后续的操作获取
在java中volatile 关键字可以保证线程间变量的可见性,当线程A对变量X进行修改的后,线程A后面执行的线程就能看到X的变化,此外volatile 在jdk1.5后,还有一个作用是阻止局部重排序的发生,也就是说volatile变量操作指令是不会被重排序的,因此使用volatile来修饰instance之后,Double-Check懒汉式单例模式就不会存在其他问题了。代码如下:
//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton2 {
private volatile static Singleton2 instance= null;//不实例化
public List<String> list = null;//list属性
private Singleton2(){
list = new ArrayList<String>();
}//构造函数
public static Singleton2 getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//第一次判断,当instance为null时,则实例化对象,否则直接返回对象
synchronized (Singleton2.class){//同步锁
if(null == instance){//第二次判断
instance = new Singleton2();//实例化对象
}
}
}
return instance;//返回已存在的对象
}
}