单例设计模式

单例模式主要是为了避免因为创建了多个实例造成资源的浪费,且多个实例由于多次调用容易导致结果出现错误,而使用单例模式能够保证整个应用中有且只有一个实例。

//单例模式的饿汉式
class Singleton1{
    private static Singleton1 instance = new Singleton1();
    //私有构造方法,防止new对象
    private Singleton1(){

    }
    //静态方法,get方法返回对象
    public static  Singleton1 getInstance(){
        return instance;
    }
}
/**
 *  访问方式  Singleton instance = Singleton.getInstance();
 *  优点:这种方式实现简单,在类加载的时候就完成了实例化,避免线程同步问题
 *
 *  缺点:由于在类加载的时候就实例化,所以没有达到懒加载的效果,也就是说可能没有用到这个实例,
 *  但是它也会加载,会造成内存的浪费。
 *
 */


//单例模式的懒汉式(线程不安全)
class Singleton2{
    private static Singleton2 instance  = null;
    //私有构造方法,防止new对象
    private Singleton2(){}
    public static Singleton2 getInstance(){
        if (instance == null){   //引发线程安全问题
            instance = new Singleton2();
        }
        return instance;
    }
}
//单例模式的懒汉式(线程安全,效率低不推荐使用)
class Singleton3{
    private static Singleton3 instance  = null;
    private Singleton3(){}
    public static synchronized Singleton3 getInstance(){  //加锁
        if (instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}
/**
 * 缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。
 * 而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了
 */

//单例模式的懒汉式(线程不安全)
class Singleton4{
    private static Singleton4 instance  = null;
    //私有构造方法,防止new对象
    private Singleton4(){}
    public static Singleton4 getInstance(){
        if (instance == null){   //同样会引发线程安全问题
            synchronized (Singleton4.class){
                instance = new Singleton4();
            }
        }
        return instance;
    }
}

//内部类方式(推荐使用),优点:线程安全;延迟加载;效率较高。

/**
 * Singleton5类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会
 * 装载SingletonHolder类,从而完成Singleton5的实例化。类的静态属性只会在第一次加载类的时候初始化,
 * 所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
 */
class Singleton5{
    private Singleton5(){}
    private static class SingletonHolder{
        private static Singleton5 i = new Singleton5();
    }
    public static Singleton5 getInstance(){
        return SingletonHolder.i;
    }
}


双检锁方式为什么还要对singleton对象用volatile关键字?(volatile关键字禁止指令重排)
/**
 * 多线程访问时,双检锁的单例模式 (推荐使用)
 * 优点:线程安全;延迟加载;效率较高。
 */
public class Singleton {
    private volatile static Singleton instance;  //为啥还要用volatile关键字
    //private  static Singleton instance;  //
    private Singleton (){}
    public static Singleton getSingleton() {
        //双重校验锁
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;  
        				   
    }
}

不用volatile变量修饰,在极端的情况下,有个隐藏的问题。
要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:

instance = new Singleton();  //编译后会变成三条指令
memroy = allocate();  //(1)分配内存空间
ctorInstance(memory);  // (2)初始化对象
instance = memory;    //(3)将内存空间的地址赋值给对应的引用

正常情况下,这3条执行时按顺序执行,双重检测机制就没有问题。但是CPU内部会在保证不影响最终结果的前提下对指令进行重新排序(不影响最终结果只是针对单线程,切记),指令重排的主要目的是为了提高效率。在本例中,如果这3条指令被重排成以下顺序:

(1)分配内存空间。

(2)将内存空间的地址赋值给对应的引用。

(3)初始化对象

这三步操作由于不是原子操作,并且java平台内存模型中有一个叫“无序写”(out-of-order writes)的机制,所以可能存在的情况是两个线程,A线程先执行到这一步,先完成了对象的实例化,还没有完成给变量赋值的时候,此时A线程退出了,然后B线程拿到锁后,因为instance对象还未完成初始化,但是已经不再指向null,最后退出,从而返回一个还未初始化完成的instance对象,从而出导致问题出现。

因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行。

volatile的使用限制

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。
volatile的使用场景
  1. 状态标记量
    使用volatile来修饰状态标记量,使得状态标记量对所有线程是实时可见的,从而保证所有线程都能实时获取到最新的状态标记量,进一步决定是否进行操作。例如常见的促销活动“秒杀”,可以用volatile来修饰“是否售罄”字段,从而保证在并发下,能正确的处理商品是否售罄。

  2. 双重检测机制实现单例
    普通的双重检测机制在极端情况,由于指令重排序会出现问题,通过使用volatile来修饰instance,禁止指令重排序,从而可以正确的实现单例。

小结
  1. 每个Java线程都有自己的工作内存,工作内存中的数据并不会实时刷新回主内存,因此在并发情况下,有可能线程A已经修改了成员变量k的值,但是线程B并不能读取到线程A修改后的值,这是因为线程A的工作内存还没有被刷新回主内存,导致线程B无法读取到最新的值。
  2. 在工作内存中,每次使用volatile修饰的变量前都必须先从主内存刷新最新的值,这保证了当前线程能看见其他线程对volatile修饰的变量所做的修改后的值。
  3. 在工作内存中,每次修改volatile修饰的变量后都必须立刻同步回主内存中,这保证了其他线程可以看到自己对volatile修饰的变量所做的修改。
  4. volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。
  5. volatile保证可见性,不保证原子性,部分保证有序性(仅保证被volatile修饰的变量)。
  6. 指令重排序的目的是为了提高性能,指令重排序仅保证在单线程下不会改变最终的执行结果,但无法保证在多线程下的执行结果。
  7. 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止重排序。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值