谈一谈java的单例模式

版权声明:随便转,随便看 https://blog.csdn.net/qq_21780681/article/details/83958954

1、什么是单例模式

引用自单例模式–维基百科
单例对象的类必须保证只有一个实例存在

单例可以分为两大类:

懒汉式:指全局的单例实例在第一次被使用时构建。
饿汉式:指全局的单例实例在类装载时构建。
–常用的应该是懒汉式,什么时候需要什么时候去创建,才能造成资源的不浪费。

2、懒汉式单例

2.1、教科书版本

/**
 * 懒汉式单例--1.0版本
 */
public class SingletonV1 {
    private static SingletonV1 singletonV1 = null;
    //构造器私有,防止被外部的类调用
    private SingletonV1() {
    }
    private static SingletonV1 getInstance() {
    	//每次获取单例对象时进行判断,如果为空,则重新new一个出来。
        if (singletonV1 == null) {
            return new SingletonV1();
        }
        return singletonV1;
    }
}

存在问题
当多线程工作时,两个线程同时运行到 if (singletonV1 == null) ,两个线程都判断为null,便会创建两个对象,就不是单例模式了。

2.2、synchronized(同步锁)版本

多线程的情况下会出现上述情况,那么加个同步锁。

/**
 * 懒汉式单例--2.0版本
 */
public class SingletonV2 {
    private static SingletonV2 singletonV2 = null;
    
    private SingletonV2() {
    }
    //加上synchronized 形成同步锁
    private synchronized static SingletonV2 getInstance() {
        if (singletonV2 == null) {
            return new SingletonV2();
        }
        return singletonV2;
    }
}

解决分析
  加上synchronized 形成同步锁。当两个线程同时运行到getInstance()这个方法时,会有一个线程A获得同步锁,继续执行,另一个线程B则需要等待,当线程A完成null的判断,对象的创建,对象的返回之后,线程B才会执行,所以就避免了多线程创建多个实例的情况。
存在问题
  虽然给getInstance()方法加锁,避免了多线程多实例的问题,但是会强制让其他线程进行等待,对程序的运行效率造成负担。

2.3、双重检查(Double-Check)版本

上述synchronized版本代码中的代码,其实没必要将整个方法进行同步锁。只需要将new这个操作进行同步处理就行了。创建对象的动作只有一次,后面的动作全是读取那个成员变量,这样做会严重影响后续的性能。
在线程同步前还得加一个(singleton== null)的条件判断,如果对象已经创建了,那么就不需要线程的同步了

/**
 * 懒汉式单例--3.0版本
 */
public class SingletonV3 {
    private static SingletonV3 singletonV3 = null;

    private SingletonV3() {
    }

    private static SingletonV3 getInstance() {
        if (singletonV3 == null) {
            synchronized (SingletonV3.class) {
                if (singletonV3 == null) {
                    singletonV3 = new SingletonV3();
                }

            }
        }
        return singletonV3;
    }
}

这里进行了两次if (singletonV3 == null)的判断

  • 第一次if (singletonV3 == null),为了解决同步锁的效率问题,只有singletonV3为空才会进行synchronized代码块。
  • 第二次if (singletonV3 == null),为了防止出现多实例的情况。

以上代码还是有些小问题先弄清楚两个东西原子操作,指令重排

3、原子操作

原子操作就是原子操作指的是不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程)。

一个简单的原子操作

m = 6;//这是一个原子操作

假设m原先值为0,对于这个操作,要么成功m的值变为6,要么没执行,m的值还是0,而不会出现其他值,而声明并赋值不是一个原子操作

int m = 6//这不是一个原子操作

对于这个,至少两个操作:

  1. 声明一个变量m
  2. 给m赋值为6

这样就会有一个中间状态
变量已经被声明,但是还没有赋值的状态。
在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能导致不稳定结果出现。

4、指令重排

指令重排意思是计算机在不影响结果的情况下,为了优化,会做一些优化,对一些语句的执行顺序进行调整。

int a ;   // 语句1 
a = 8 ;   // 语句2
int b = 9 ;     // 语句3
int c = a + b ; // 语句4

正常来说,执行顺序是自上而下。由于指令重排的原因吗,因为不影响结果,执行顺序可能成为3124或者1324。由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。–也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

主要在于singletonV3= new singletonV3()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1.给 singletonV3分配内存

  2.调用 SingletonV3 的构造函数来初始化成员变量,形成实例

  3.将singletonV3对象指向分配的内存空间(执行完这步 singletonV3才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 singletonV3,已经是非 null 了(但却没有初始化),所以线程二会直接返回 singletonV3,然后使用,然后顺理成章地报错。

5、volatile版本

给instance的声明加上volatile关键字

/**
 * 单例模式4.0版本
 */
public class SingletonV4 {
    //volatile仅在jdk1.5后使用
    private static volatile SingletonV4 singletonV4;

    private SingletonV4() {
    }

    private static SingletonV4 getInstance() {
        if (singletonV4 == null) {
            synchronized (SingletonV4.class) {
                if (singletonV4 == null) {
                    singletonV4 = new SingletonV4();
                }
            }
        }
        return singletonV4;
    }
}

volatile关键字的作用为禁止指令重排,把singletonV4声明为volatile,对它的读写就有一层内存屏障,这样在它赋值完成之前,就不会对它执行读的操作。
内存屏障
    内存屏障(memory barrier)和volatile什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。

volatile阻止的不是*singletonV4 = new SingletonV4()*这个的指令重排,而是保证了在写操作完成之前,不会调用读的操作。

6、饿汉式单例

饿汉式单例是指:指全局的单例实例在类装载时构建的实现方式。
由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。

/**
 * 饿汉式单例
 */
public class SingletonV5 {
    private static final SingletonV5 INSTANCE = new SingletonV5();

    private SingletonV5() {
    }

    private static SingletonV5 getInstance() {
        return INSTANCE;
    }

}

缺点
INSTANCE的初始化是在类加载中进行,而类加载由ClassLoader执行,对开发者而言很难把握初始化的时机:
1.可能初始化太早,造成资源的浪费;
2.如果初始化本身依赖于一些其他数据,那么很难保证需要的数据,在其初始化完成时是否准备好。

7、枚举实现单例方式

/**
 * 枚举单例的实现
 */
public enum SingletonV6 {
    INSTANCE;

    public void FunTest() {
        //...
    }
}

作者对这个方法的看法
这种方法在功能上与公有域方法相近,但是比它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是面对复杂的序列化或者反射攻击的时候。虽然这种方法还没广泛使用,但是单元素的枚举类型已经实现singleton的最佳方法

猜你喜欢

转载自blog.csdn.net/qq_21780681/article/details/83958954
今日推荐