单例模式--饿汉模式, 懒汉模式

本文详细介绍了Java中的单例模式,包括饿汉模式(类加载即实例化)和懒汉模式(延迟实例化),并讨论了它们在多线程环境下的线程安全问题,以及如何通过加锁和volatile关键字进行优化。

单例模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.

单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.

饿汉模式

类加载的同时, 立即创建实例

class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

private static Singleton instance = new Singleton();: 带有static的属性称为类属性, 由于每个类的类对象(类名.class)是单例的, 则类对象的属性也就是单例的了. 这个代码时Singleton类被JVM加载的时候执行的.

将_构造方法置成private_, 就可以防止该类在类外被创建的情况, 也就可以形成单例模式.

如何创建实例?

public class Main {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        //Singleton s2 = new Singleton();报错
    }
}

Singleton.getInstance得到的一直是同一个对象.

在多线程中, 如果有多个线程同时调用getInstance方法, 他读到的都是同一个变量, 所以

饿汉模式是线程安全的

懒汉模式

类加载的时候不创建实例. 第一次使用的时候才创建实例.

一般情况下, 懒汉模式一般效率比较高.

例如, 编辑器要打开一个超大的文件(10G), 有的编辑器会尝试把所有的内容都加载到内存中, 再显示出来; 而有的编辑器则只加载一部分内容, 其他部分, 等用户需要用到了再加载

class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy() {}
}
public class Main {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
    }
}

很明显, 这里是调用getInstance后才创建实例.

那么如果多个线程同时调用getInstance, 就会很容易引发线程安全问题, 因为上述代码的赋值操作并不是原子的, 所以懒汉模式是线程不安全的

如何解决线程安全问题

我们可以想到:加锁, 让赋值操作变成原子性的.

但是加锁是一个成本比较高的操作, 加锁可能会引起阻塞等待, 所以非必要不加锁. 如果无脑加锁, 就会导致程序执行效率受到影响. 所以这里该如何加锁?

class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        synchronized (SingletonLazy.class) {
            if(instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
    private SingletonLazy() {}
}

如果是上述这样加锁, 那么每一次执行到这个代码的时候都会进行加锁, 但这并不是必要的, 因为引发线程不安全, 是在首次new对象的时候, 一旦把对象new好了, 就不会再出现线程不安全的问题了. 所以这样写会降低效率.

所以只需在instance是null, 需要new对象的时候才需要进行加锁. 所以代码应该如下:

class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        if(instance == null) {
            synchronized (SingletonLazy.class) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {}
}
T1T2
t1if(instance == null)
t2if(instance == null)
t3synchronized (SingletonLazy.class)阻塞等待
t4if(instance == null)…
t5释放锁synchronized (SingletonLazy.class)
t6if(instance == null)

问题又来了, T2线程一定能读到T1线程修改的值吗?

所以这里可能又存在了一个内存可见性问题, 所以需要给instance加上一个volatile

class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        if(instance == null) {
            synchronized (SingletonLazy.class) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {}
}

同时, volatile还可以解决指令重排序的问题

instance = new SingletonLazy();这个操作可以分成三步:

  1. 给对象创造出内存空间, 得到内存地址.
  2. 在内存空间上调用构造方法, 对对象进行初始化
  3. 把内存地址复制给instance引用.

一般的执行顺序使1->2->3, 但也可能会被优化成1->3->2. 假设T1是按照1->3->2执行的, 并且在执行完3后2之前出现了线程切换, 此时还未对对象进行初始化就调度给别的线程了. T2线程执行的时候, 判断instance非空, 于是直接返回instance, 并且后续可能会使用到instance中的一些属性和方法, 这样就可能会出现一些问题.

给instance加上volatile之后, 针对instance的操作就不会发生指令重排序的问题了.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是布谷阿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值