概述
简单来说,单例模式的作用就是保证整个应用程序的生命周期中,任何一个时刻,单例类的实例都只存在一个或者不存在。
单例模式确保某个类只有一个实例,而且自行实例化,并向整个系统提供整个实例的单例模式。
如果要写一个单例模式的代码第一件事情就是自己要清除你到底写的是饿汉式还是懒汉式的单例
饿汉式: 不管用不用,先提前创建对象。(使用不当线程不安全、节省空间)
懒汉式: 延迟加载,用到的时候再去创建对象。(线程安全,浪费内存)
三种线程安全的懒汉式(延时加载)单例写法
写一个线程安全的单例模式最重要的三点:
- 构造方法要私有
- 静态成员变量存储唯一的实例化对象
- 提供静态的公开的方法去获取实例对象
静态内部类的单例写法
public class SingletonStatic {
//构造方法要私有
private SingletonStatic(){}
//唯一的静态实例
private static class SingletonFactory{
private static SingletonStatic one = new SingletonStatic();
}
//公开方法
public static SingletonStatic getSingleton(){
return SingletonFactory.one;
}
}
简单来说,在jvm类加载过程中的初始化阶段,也就是虚拟机执行<clinit>()方法的过程中是线程安全的,虚拟机只允许一个线程去执行<clinit>()方法,其他线程都会被阻塞。详细类加载过程可以看另一篇虚拟机的类加载过程,又因为它是静态内部类,只会被初始化一次,所以它满足单例的要求。
饿汉式与懒汉式的区别式在于对对象创建的时机,对于静态内部类来说,只有在第一次调用getSingleton()方法的时候才会去初始化静态内部类,实例对象才会被创建,所以说它是属于饿汉式的。
枚举单例写法(最简单)
public enum SingletonEnum {
ONE;
public void say(){
System.out.println("枚举单例测试"+this.hashCode());
}
}
双重检查锁(DCL)写法(最重要的)
我们来看这段代码
这段代码看似符合单例模式,但其实只有在单线程下它确实是没有问题的,我们考虑一下,当50个线程同时到达箭头所指方向的时候,如果不加锁,那么就会在Java堆中创建50个one对象所需的内存大小,但由于它是静态对象,在栈中只会有一个该对象的符号引用,所以就有49个内存大小会被浪费,着显然是不合理的。这个问题如何解决呢?最简单的就是使用Synchronized。
这样上面的问题就解决了,当同时50个线程到达加锁的方法的时候,将发生如下的情况:
这样又会造成多个线程抢占到CPU时间片的时候去发呆,CPU的时间是非常非常宝贵的,这样浪费显然又不合理,那么我们接着优化,我们看看下面两个代码:
这个代码其实和代码3是一样的,既然是优化,当然不能这样,我们来看看代码5
为什么加第一次判断呢,这里很明显只有第一次才能进入下面的代码块,而其他线程直接放回的就是one对象,这样很大程度上节省了cpu发呆时间。
那么为什么加第二次的判断呢,命名加了synchronized只有一个线程才可以执行这段代码啊?我们来看去掉后的代码
public class SingletonDCL {
//构造私有
private SingletonDCL(){}
//私有的静态变量存储唯一实例,懒汉,暂不初始化
private static SingletonDCL one = null;
//公开方法
public static SingletonDCL getSingleton(){
//没有对象再去创建
if(one!=null){
synchronized (SingletonDCL.class) {
one = new SingletonDCL();
}
}
return one;
}
}
这里假设5个线程同时来竞争锁,线程一先拿到锁,实例化对象完成后,这时候线程二又拿到锁,实例化对象,这样的话又同时创建了4个无用对象。和上面的情况是一样的。因次双层检查锁两次检查都必不可少。
我们对代码5再假设:线程一执行到了one = new SingletonDCL();这里注意这段代码并不是一个指令,这时线程二执行到了第一个判断处,而这里线程一new过程中new了,此时one不为null,但是new这个过程又没有执行完成(这部分可以了解对象的实例化过程),而线程二直接返回了不完整的one对象,这就有了问题,这就是我们常说的有序性的问题。
解决办法大家应该也都知道,就是volatile关键字,它能够使得jvm进制指令重排序。
因此最终版的双检索单例模式代码如下:
public class SingletonDCL {
//构造私有
private SingletonDCL(){}
//私有的静态变量存储唯一实例,懒汉,暂不初始化
private static volatile SingletonDCL one = null;
//公开方法
public static SingletonDCL getSingleton(){
//没有对象再去创建
if(one!=null){
synchronized (SingletonDCL.class) {
one = new SingletonDCL();
}
}
return one;
}
}
如何破坏一个单例模式
- 反射破坏单例模式
- 反序列化破坏单例模式
其实枚举就是最简单的且天然支持反射攻击的一种单例模式。
接下来附上一个可以防止序列化攻击破坏单例模式的完整的双检索双检索单例模式
public class SingletonDCL {
//构造私有
private SingletonDCL(){}
//私有的静态变量存储唯一实例,懒汉,暂不初始化
private static volatile SingletonDCL one = null;
//公开方法
public static SingletonDCL getSingleton(){
//没有对象再去创建
if(one!=null){
synchronized (SingletonDCL.class) {
one = new SingletonDCL();
}
}
return one;
}
private Object readResolve(){
return instance;
}
}
简单分析并发编程中可见性、有序性、原子性
有序性
有序性:在微观上指的是CPU指令的执行顺序,宏观上指的是java代码的执行顺序。
当两段代码的执行结果没有必然联系的话,JVM内部可能会对其进行指令重排序,这是JVM自身优化的一种策略。
原子性
原子性就是需要保证某个操作是一个原子操作:实现方式synchronized、lock锁。
可见性(注意的是只有多核系统才会有可见性的问题)
每次CPU都从内存中读取数据到CPU缓存,当进行i++操作时,CPU1从内存读取i=10,进行+1操作,这时候CPU1缓存中i=11,此时内存中i=10,而CPU2从内存再读取的话又是读取到了10,进行+1操作的话CPU2缓存中又是11
其实这个时候正确的应该时CPU2读取到11再+1,i的值应该时12,这就是可见性的问题(额外:MESI协议,保证CPU见缓存一致性问题)
volitale关键字:被这个关键字的修饰的变量,当CPU在读数据时,必须保证其他CPU缓存中的最新值被回刷回内存中去,这样的话就能保证每次CPU读取的值都是最新值了。