- 私有化构造器,不允许通过构造器去创新实例。
- 提供一个公共的静态方法,通过调用这个方法来返回唯一的实例
写法-饿汉写法
//final是一个标识,表明不应该被继承,其实也可以吧构造器设计为私有的
final public class 饿汉模式 {
private static 饿汉模式 instance = new 饿汉模式();
private 饿汉模式(){}
public static 饿汉模式 getInstance(){
return instance;
}
}
优点:由于在加载类的时候就创建,而且使用了static修饰,那么这个实例只会被初始化一次。那么在多线程的环境下这个实例对象也是线程安全的。
缺点:没有判断是否真的需要这个实例对象,而是直接创建,造成内存空间的浪费。
写法-懒汉模式
final public class 懒汉模式 {
private static 懒汉模式 instance = null;
private 懒汉模式() {
}
public static 懒汉模式 getInstance() {
if (instance == null) {
instance = new 懒汉模式();
}
return instance;
}
优点: 节省空间, 用到的时候再创建实例对象.
需要这个实例的时候, 先判断它是否为空, 如果为空, 再创建单例对象.
用到的时候再去创建, 与JVM加载类的思路一致: 都是需要的时候再处理.
(2) 缺点: 线程不安全.
① 在并发获取实例的时候, 线程A调用getInstance(), 在判断instance == null时得到true的结果, 之后进入if语句, 准备创建instance实例;
② 恰好在这个时候, 另一个线程B来了, CPU将执行权切换给了B —— 此时A还没来得及创建出实例, 所以线程B在判断instance == null的时候, 结果还是true, 所以线程B也会进入if语句去创建实例;
③ 问题来了: 两个线程都进入了if语句, 结果就是: 创建了2个实例对象.
线程安全测试
public class 测试线程安全 {
// 同步的Set, 用来保存创建的实例
public static void main(String[] args) {
Set<String> instanceSet = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < 100; i++) {
new Thread(() ->{
instanceSet.add(懒汉模式.getInstance().toString());
}
).start();
}
for (String instance : instanceSet){
System.out.println(instance);
}
}
}
运行结果:
可以看到创建了两个对象,打破了单例模式的单例设计。所以我们可以加上同步安全机制。使用synchronized修饰方法
final public class 懒汉模式 {
private static 懒汉模式 instance = null;
private 懒汉模式() {
}
public synchronized static 懒汉模式 getInstance() {
if (instance == null) {
instance = new 懒汉模式();
}
return instance;
}
}
这个时候就只会创建一个对象了。
写法-双重检查锁机制
由于我们上面直接对象获取实例的方法加锁,导致某个线程访问这个方法时,每个线程都会被挂起等待,比较浪费资源。
所以我们应该在实例化前,先判断是否需要加锁,也就是加上一层判断是否要进行加锁。
final public class 双重检查锁模式 {
private static 双重检查锁模式 instance = null;
private 双重检查锁模式() {
}
public static 双重检查锁模式 getInstance(){
//判断是否需要加锁
if(instance == null){
synchronized (双重检查锁模式.class){
//在进行一次判断,防止其他线程在等待锁的过程中,创建了多个实例。由于其他线程可能都走完第一个判断了
if(instance == null){
return new 双重检查锁模式();
}
}
}
return instance;
}
}
第一次判断是为了确认是否需要进行加锁,因为我们要进行实例化了。
第一次判断是否为了防止其他线程走完第一个判断,都在等待锁被释放,当锁被释放,这个锁就会被其他线程持有。如果不再进行一次判断的话,就会导致创建出新的实例对象出来。
但是,这里其实也是还有问题。
(1) JVM在创建实例的时候, 是分为如下步骤创建的:
① 在堆内存中, 为新的实例开辟空间;
② 初始化构造器, 对实例中的成员进行初始化;
③ 把这个实例的引用 (也就是这里的instance) 指向①中空间的起始地址.
==> 也就是说, Java中创建一个对象的过程并不是原子性操作.
(2) 上述过程不是原子性的, 所以就可能出现:
JVM在优化代码的过程中, 可能对①-③这三个过程进行重排序 —— 因为 JVM会对字节码进行优化, 其中就包括了指令的重排序.
如果重排序后变为①③②, 就会出现一些难以捕捉的问题.
(3) 再来说说半个对象:
构造方法中有其他非原子性操作, 创建对象时只是得到了对象的正确引用, 而对象内部的成员变量可能还没有来得及赋值, 这个时候就可能访问到 "不正确(陈旧)" 的成员变量.
对引用类型 (包括对象和数组) 变量的非同步访问, 即使得到该引用的最新值, 也并不能保证能得到其成员变量 (对数组而言就是每个数组中的元素) 的最新值;
解决办法:
加上volatile,禁止JVM对指令进行重排序。
private volatile static 双重检查锁模式 instance = null;
volatile关键字禁止了JVM的指令重排序, 并且保证线程中对这个变量所做的任何写入操作对其他线程都是即时可见的 (也就是保证了内存的可见性).
写法内部类写法(比较推荐)
我们把这个唯一的实例写在静态内部类里面,然后在静态内部类里面创建实例。那么这个实例只会被调用静态内部类的时候加载一次。
final public class 静态内部类写法 {
private 静态内部类写法(){}
public static 静态内部类写法 getInstance(){
return SingleHolder.instance;
}
private static class SingleHolder{
private static 静态内部类写法 instance = new 静态内部类写法();
}
}
JVM加载外部类过程中,是不会加载静态内部类的,只有内部类的的属性/方法被调用时才会被加载,并初始化其静态属性。也是懒汉模式的变种。
static不可以修饰顶级类哦!!
但是static可以用来修饰内部类
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1vq20gbk2eut1