前言
今天听完狂神说java的单例模式,感慨万分。希望通过这篇文章跟大家一起学习。
1. 饿汉式
[1] 一个私有的构造器
[2] 一个静态变量
[3] 一个静态方法返回对象
饿汉式单例是直接使用静态变量的方式生成这个单例对象(不管是否调用) , 所以缺点比较明显就是占用空间。
package com.gs.juc.单例模式;
//饿汉式单例
public class Hungry {
//可能会造成浪费空间(假设此时有相应的变量生成)
//private byte[] data1 = new byte[1024*1024];
// 1. 一个私有的构造器
private Hungry(){
}
//2.一个静态变量
private final static Hungry HUNGRY = new Hungry();
//3.一个静态方法
public static Hungry getInstance(){
return HUNGRY;
}
}
2. 懒汉式
懒汉式:表示使用的时候再去创建对象,又叫做DCL(Double Check Lock即双重检测锁模式)
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+" ok");
}
private static LazyMan lazyMan;
//双重检测锁模式(DCL双重检测锁模式)
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
//多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
if(lazyMan==null){
lazyMan = new LazyMan(); //不是一个原子操作
}
}
}
return lazyMan;
}
<1> 那么我们就会有疑问了,这种方式在多线程的环境下就已经安全了吗?
其实并不然,因为这个lazyMan = new LazyMan();的操作并不是一个原子操作,它底层是分为三步去执行的。
[1] 分配内存空间
[2] 执行构造方法,初始化对象
[3] 把这个对象指向内存空间
我们期待的执行顺序当然是[1][2][3],可是计算机底层为了提高效率,存在一种指令重排的操作。在不影响这个操作最终结果的前提下,它可以对这三个执行步骤进行重新排序,也就是它可能会变成[1][3][2],那这种情况会带来什么影响呢?如果我们在多线程的环境下执行,当A线程执行了[1] [3] 操作后,B线程进来了,因为这时lazyMan已经有相应的对象了,那么B线程不会走判断的步骤,而是直接返回 lazyMan这个对象,可是A线程并没有初始化完成,这时返回的就是一个空对象。
解决方法:为了防止指令重排其实很简单,就是在变量上加上一个 volatile关键字(volatile可以通过内存屏障防止指令重排)
只需改变上面的一行代码即可:
private volatile static LazyMan lazyMan;
<2> 看一看这个单例其实已经很完美了,可是我们却忘记了反射这个操作,在反射面前似乎又并不安全。
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+" ok");
}
private static LazyMan lazyMan;
//双重检测锁模式(DCL双重检测锁模式)
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
//多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
if(lazyMan==null){
lazyMan = new LazyMan(); //不是一个原子操作
}
}
}
return lazyMan;
}
//-------------在上面的基础上增加的代码-----------
//通过反射破坏单例
public static void main(String[] args) throws Exception {
//我们先通过单例的方法获取一个实例
LazyMan instance = LazyMan.getInstance();
//再通过一个反射获取另外一个实例
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//设置权限
declaredConstructor.setAccessible(true);
LazyMan instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
}
结果:这时你会发现单例模式被破坏了,因为生成了两个不同的对象
解决方式:可以在构造器的时候,引入一个判断(三重判断), 检查lazyMan这个对象是否已经存在
public class LazyMan {
// -------和上面代码有所区别的地方---------
private LazyMan() {
synchronized (LazyMan.class){
if(lazyMan==null){
System.out.println(Thread.currentThread().getName() + " ok");
}else{
throw new RuntimeException("不要试图通过反射破坏单例");
}
}
}
private static LazyMan lazyMan;
//双重检测锁模式(DCL双重检测锁模式)
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
//多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
if(lazyMan==null){
lazyMan = new LazyMan(); //不是一个原子操作
}
}
}
return lazyMan;
}
//通过反射破坏单例
public static void main(String[] args) throws Exception {
//我们先通过单例的方法获取一个实例
LazyMan instance = LazyMan.getInstance();
//再通过一个反射获取另外一个实例
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//设置权限
declaredConstructor.setAccessible(true);
LazyMan instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
}
结果:很nice,确实做到防止反射破坏单例了,这时我又觉得自己行了。
<3> 可是如果细想,如果两个实例都通过反射获得,那么自然不会存在lazyMan这个判断的阻挠。我们把代码往下改:
public class LazyMan {
private LazyMan() {
synchronized (LazyMan.class){
if(lazyMan==null){
System.out.println(Thread.currentThread().getName() + " ok");
}else{
throw new RuntimeException("不要试图通过反射破坏单例");
}
}
}
private static LazyMan lazyMan;
//双重检测锁模式(DCL双重检测锁模式)
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
//多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
if(lazyMan==null){
lazyMan = new LazyMan(); //不是一个原子操作
}
}
}
return lazyMan;
}
//----------与上面有所区别的地方---------
public static void main(String[] args) throws Exception {
//两个对象都通过反射获得
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//设置权限
declaredConstructor.setAccessible(true);
LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
}
结果:gg, 还有这么赖的方式创建对象,那我们的单例不是又被破坏了吗
解决方式:我们在学习并发编程的时候,学过一种信号灯的方式,简单的说就是设置一个标志位flag为false,当有线程走了LazyMan()这个私有化构造器先判断这个标志位,若为false,则放行,并把标志位置为ture。下次有线程进来就直接被拦截了。
public class LazyMan {
// --------- 和上面有所区别的地方------------
//设置一个标志位
private static boolean flag = false;
private LazyMan() {
synchronized (LazyMan.class) {
//优化4:通过标志位去避免两次反射创建对象的情况
if (flag == false) {
flag = true;
System.out.println(Thread.currentThread().getName()+" ok");
} else {
throw new RuntimeException("不要试图破坏反射");
}
}
}
private static LazyMan lazyMan;
//双重检测锁模式(DCL双重检测锁模式)
public static LazyMan getInstance(){
if(lazyMan == null){
synchronized (LazyMan.class){
//多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
if(lazyMan==null){
lazyMan = new LazyMan(); //不是一个原子操作
}
}
}
return lazyMan;
}
//----------与上面有所区别的地方---------
public static void main(String[] args) throws Exception {
//两个对象都通过反射获得
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//设置权限
declaredConstructor.setAccessible(true);
LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
}
我好像又可以了,可是反射就不能获得你标志位的属性吗?停,刚精快醒醒。别试了,在反射面前所有的属性和方法都是透明,所以理论上都是不安全的。在java中有一个类天生就是单例安全的,没错它就是枚举类。为什么它安全呢?因为它可以避免反射的操作。
反射中newInstance() 上对枚举类的说明:
3. 枚举类的单例
package com.gs.juc.单例模式;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
//枚举是一个什么? 本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws Exception {
//1.通过枚举
EnumSingle instance1 = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.INSTANCE;
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
结果:观察两个对象的hash值,确实是单例安全的。
4. 静态内部类实现单例模式
说明:这个方式实现的单例和前面的懒汉式和饿汉式都是线程不安全的
package com.gs.juc.单例模式;
//静态内部类
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
小结:
单例模式的这4种实现方式,本质上各有各的优势。其中我们使用懒汉式的单例时,一般只要实现到避免指令重排那一步即可。因为深究反射可以让类中的方法和属性都透明化。(后面关于反射的优化很多只是为了面试时能多说一点)使用饿汉式的单例时,虽然占用内存,不过实现简单。