小白自学设计模式之单例模式

说句实在的单例模式是最为常见的java编程开发中的设计模式,绝大多数和我一样的小白大多可能对饿汉模式和懒汉模式比较了解了,精进的话需要了解和使用双重校验模式,静态内部类单例包括枚举,容器方式。

单例的概念:确保某一个类只有一个实例对象,而且自行实例化(new Instance( ) )并向整个系统提供这个实例对象。

优势: 在内存中只有一个实例,减少了内存的开支,特别是一个对象需要频繁的创建,销毁时。避免了对资源的多重占用。

当然还有Java 编程规范提倡的静态内部单例模式以及枚举单例。

回顾一下:

/**
 * 饿汉式
 *      特点:直接在类加载的时候实例化对象
 *          缺点: 违背了类加载的原则,造成类加载性能较低,没做到随用随加载
 *                无法法做到延迟创建对象,事实上如果该单例类涉及资源较多,
 *    创建比较耗时间时,我们更希望它可以尽可能地延迟加载
 */
public class HgSingleInstance {
    private  static HgSingleInstance mInstance = new HgSingleInstance();
    private HgSingleInstance(){}
    public static HgSingleInstance getInstance(){
        return mInstance;
    }
}
/**
 *  懒汉式:
 *         特点: 用的时候再加载
 *              需要使用synchronized加锁的方式,不然会产生线程同步访问的问题
 *              然而整个方法加锁会造成性能很低下的问题。
 */
public class LazySingleInstance {
    private static LazySingleInstance mInstance;
    private LazySingleInstance(){}
    public static synchronized LazySingleInstance getInstance(){
        if (mInstance == null){
            mInstance = new LazySingleInstance();
        }
        return mInstance;
    }
}

接下来看下我们的主角 DCL双重校验单例模式,规避了以上的问题,但仍然存在指令集乱序的问题。

/**
 * 双重校验: 不为空直接返回
 *          当为空时即第一次初始化,只需在这时候进行加锁校验,只需并发一次同步即可
 *          效率性能都较为优秀
 */
public class DCLSingle {
    private static volatile DCLSingle mInstance = null;
    private DCLSingle(){}
    public static DCLSingle getInstance(){
        if (mInstance == null){
            synchronized (DCLSingle.class){
                if (mInstance == null){
                    mInstance = new DCLSingle();
                }
            }
        }
        return mInstance;
    }
}

解释:

这种编写方式被称为“双重检查锁”,主要在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。保证从主内存中读取。

为什么要加入volatile

假如线程A执行到 instance = new Singleton()语句,这里看起来是一句代码但其实他并不是一个原子操作,这句代码最终会被编译成许多条编译指令。

A. 给Singleton的实例分配内存  B. 调用singleton()的构造函数,初始化成员字段

C. 将sInstance对象指向分配的内存空间[此时mInstance就不是null] 

但是由于java编译器允许处理器乱序执行, B和C的顺序是无法保证的

假如线程1执行 A-C-B  当执行到C时刚好线程2进来拿到instance不为空,但是其并未初始化,就会出问题。

当然还有内部类和枚举单例的方式,后面再去进行学习。

 

静态内部类单例模式: 最优秀

<<Java并发编程实践>>推荐

/**
 * Created by zxl on 2018/8/11.
 * 思考静态方法的执行顺序
 *
 * 被static 修饰的成员 类 方法只能被加载一次
 */
public class StaticSingleInstance {
    private StaticSingleInstance(){

    }

    public static StaticSingleInstance getInstance(){
        return StaticHolder.INSTANCE;
    }

    private static final class StaticHolder{
        private static final StaticSingleInstance INSTANCE = new StaticSingleInstance();
    }
}

public class Client {
    public static void main(String[] ars){
        StaticSingleInstance instance1 = StaticSingleInstance.getInstance();
        StaticSingleInstance instance2 = StaticSingleInstance.getInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
    // output: eve_8_11.StaticSingleInstance@74a14482
    //         eve_8_11.StaticSingleInstance@74a14482
}

说明:当第一次加载Singleton类时并不会初始化instance,只有在第一次调用singleton的getInstance时才会导致instance被初始化。因此,第一次调用getInstance方法会导致虚拟机加载StaticHolder类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化.

附属

在反序列化的情况下会重新创建对象,用于将实例对象写到磁盘,然后再读回来。类中具有一个私有的readResolve()函数,这个函数可以让开发者控制对象的反序列化。设置 serialVersionUID = 0L 新增修改字段不会抛异常。

在虚拟机第一次加载ContextImpl时会注册各种ServiceFecher,这些服务会以键值对的形式存储在一盒HashMap中,用户使用时只需要根据key来获取对应的ServiceFecher。就是利用了容器来进行缓存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值