设计模式之单例模式

单例模式

       一个类最多只有一个实例, 并且限制只能有一个实例的设计模式就是"单例模式". 这里的限制主要是通过编程语言本身的语法特性, 强行限制某个类只能实例化一个对象.

       static关键字就是天然的好手, 使用static修饰后, 对象就变成类对象, 类对象其实就是单个实例, 只会存在一个.

       单例存在的具体解释: 类对象是通过JVM的.class文件加载获得的, .class文件只会加载一次, 所以也就只有一个类对象, 类对象上面的static成员也就只有一份了. 类属性在整个进程中都是独一无二的. (类对象是以进程为单位的?)

      

单例模式的基本形式如下:

class SingleDog {

    private static SingleDog singleDog = new SingleDog();

    public static SingleDog getSingledog() {
        return singleDog;
    }

    private SingleDog() {}

}

创建对象的时候就通过private和static创建, static保证对象是属于类的, private修饰构造方法, 防止在类外被调用创建新的实例, 只能通过调用公开静态的getSingledog才能获得唯一单例.

"饿汉模式""懒汉模式"

上代码, 直接在类加载阶段就创建好了单例的实例, 就是"饿汉模式"的例子.

反之"懒汉模式"就是不那么着急, 在有需要的时候再创建单例.如下:

class SingleLazyDog {

    private static SingleLazyDog singleLazyDog = null;

    public static SingleLazyDog getSingleLazyDog() {
        if (singleLazyDog == null) {
            singleLazyDog = new SingleLazyDog();
        }
        return singleLazyDog;
    }

    private SingleLazyDog () {}

}

懒汉模式可以避免再初期初始化时候占用过多的资源.

如何在满足线程安全的前提下创建单例类?

      "饿汉模式"在类加载阶段就创建了单例对象, 过后在调用getter时候只有读取操作, 没有修改操作(这里的修改指的是创建), 所以"饿汉模式"天然是线程安全的.

      但是"懒汉模式就不一样了", 首先上面这种写法, 一方面是有获取比较的过程, 一方面是有创建实例的过程(既涉及读也涉及写), 这一波操作都不是原子的, 所以可能会在多个线程同时调用getter方法时候出现冲突, 导致创建多个实例, 破坏了单例模式的规则.

最简单粗暴的形式是直接加锁:

class SingleLazyDog {

    private static SingleLazyDog singleLazyDog = null;

    public static SingleLazyDog getSingleLazyDog() {
        synchronized (SingleLazyDog.class) {
            if (singleLazyDog == null) {
                singleLazyDog = new SingleLazyDog();
            }
        }
        return singleLazyDog;
    }

    private SingleLazyDog () {}

}

在一个线程调用getter的时候, 直接对类对象加锁, 就不会发生两个线程同时实例化的结果, 这样确实保证了线程安全. 但是也出现了新的问题, 就是导致调用getter的时候都需要进行一次加锁开锁的操作. 加锁的开销可能会涉及用户态到内核态的切换, 即成本是相对较高的, 能避免还是尽量避免. 解决方法也简单, 就是在需要的时候加锁, 不需要的时候不加呗:

class SingleLazyDog {

    private static SingleLazyDog singleLazyDog = null;

    public static SingleLazyDog getSingleLazyDog() {
        if (singleLazyDog == null) {
            synchronized (SingleLazyDog.class) {
                if (singleLazyDog == null) {
                    singleLazyDog = new SingleLazyDog();
                }
            }
        }
        return singleLazyDog;
    }

    private SingleLazyDog () {}

}

看似是已经解决问题了, 但实际上对于有实例化对象(new)操作的步骤, 还会存在"指令重排序"的问题.

我们需要先了解new操作的步骤, 本质上大概是三个:

  1. 申请内存, 得到该内存的地址
  2. 调用构造方法初始化实例
  3. 把内存首地址赋值给实例引用

出现指令重排序问题, 主要是因为这个程序操作和内存IO有关, 和内存IO有关, 编译器就可能会进行自主优化, 比如"指令重排序", 比如使用"工作内存"等等.

这里的2步骤和3步骤, 要是2和3调换了顺序, 就相当于内存确实被创建好了, 但是没有对应的实例, 是个空有的无效的内存空间.

解决这个问题, 用个volatile修饰singleLazyDog就好

private volatile static SingleLazyDog singleLazyDog = null;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值