单例模式对于广大开发者来说都不陌生了,单例模式也分为饿汉模式,懒汉模式,本文主要讲解的是volitale+synchronized修饰的单例模式来保证并发情况下的系统安全.
首先介绍下以前用的单例模式
1:不加锁的单例模式
1.1:懒汉单例模式
懒汉:顾名思义就是比较懒,只有需要的时候才会创建这个对象.
public class Singleton{
private static Singleton singleton=new Singleton();
private Singleton(){
}
public static Singleton getSingleton(){
if(singleton==null){
singleton=new Singleton();
}
return singleton;
}
}
1.2:饿汉单例模式
饿汉:就像看见喷香的肉一样激动.不管食物有没有毒,先吃了再说. 对于编程来说不管有没有对象,先创建了再说.
public class Singleton{
private static Singleton singleton=new Singleton();
private Singleton(){
}
public static Singleton getSingleton(){
return singleton;
}
}
1.3:两者的对比与不足之处
懒汉单例:代码实现简单,但没有加synchronized锁,线程不安全,在并发情况下不能正常运行。
饿汉单例:线程安全,没有加锁,执行效率会提高。在类加载时就初始化,会造成内存浪费。
2:volatile+synchronized的双锁校验单例模式
ublic class Singleton{
//采用volatile关键字来保证并发线程下的指令重排
private static volatile Singleton singleton;
private Singleton(){
}
public static Singleton getSingleton(){
if(singleton==null){
//采用synchronized来进行加锁
synchronized(Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。可以看到这个单例模式多了一个volatile和synchronized关键字来保证并发情况下的安全。那么问题来了,为什么这两个关键字就能保证并发的安全呢。下面讲解一下这两个介绍一下这两个关键字的作用。
2.1:volatile的特性
volatile是java中的关键字可以用来修饰变量。它可以保证变量的可见性以及防止指令重排。
可见性就是指一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(
禁止进行指令重排序就要从创建对象的过程来说起了。
创建一个对象可以分三个步骤:
1.分配内存对象空间
2.初始化对象
3.设置instance指向刚分配的内存地址,此时instance != null
2和3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是在多线程的情况下2和3的顺序就会颠倒。这个时候对象还没有初始化完成,就返回了,所以会有安全问题。而volatile能防止这种情况发生。
2.2:synchronized的特性
synchronized是java中常见的锁关键字,在jdk1.6以后进行了大量优化,大幅提高了执行效率。
这个关键字内容较多,后续会专门进行介绍,目前只介绍单例模式中这个关键字的作用。
synchronized修饰的代码有几种:
修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象;
修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;
而在本单例中代码为 synchronized(Singleton.class){} 这一行。锁住了这个类所对应的对象,就实现了全局锁的效果。这个时候安全的单例模式就明白了。
本文主要是从我个人的理解角度来进行解释。思考不合理或者有问题的地方,欢迎指正.