单例模式分析总结

332 阅读9分钟

测试类

 public static void main(String[] args) {
     // 创建一个可以存放20个线程的线程池(最好手动创建线程池)
     ExecutorService threadPool = Executors.newFixedThreadPool(20);
     for (int i = 0; i < 20; i++) {
         // 执行创建线程(Runnable接口)
         threadPool.execute(new Runnable() {
             @Override
             public void run() {
                 System.out.println(Thread.currentThread().getName()+":"+Singleton.getInstance());
             }
         });
     }
     // 关闭线程池
     threadPool.shutdown();
 }

1.饿汉模式

public class Singleton {
    // 构造私有的静态成员变量(实例)
    private final static Singleton singleton = new Singleton();

    // 构造函数私有化
    private Singleton(){}

    // 构造得到实例的静态方法
    public static Singleton getInstance() {
        return singleton;
    }
}
  • 1.为什么说饿汉式单例模式是线程安全的

    饿汉式单例模式是在类加载的时候创建实例(静态成员变量用static修饰),所以线程是安全的。请点击:类加载过程的线程安全性保证

  • 2.final关键字作用:

    参考地址final关键词是代表此变量一经赋值,其指向的内存引用地址将不会再改变。

    加final也仅仅是表示类加载的时候就初始化对象了,比不加载final要早一点。

    如果存在释放资源的情况下,就不能加final修饰了,释放资源之后,如果需要重新使用这个单例,就必须存在重新初始化的过程,而final定义的常量是不能重新赋值的,所以不能加final,对于不需要释放资源的情况,可以加final。

    对于需要释放资源饿汉单例模式的代码(释放资源代码参考如果此时singleton被声明为final,IDE会提示编译错误,因为singleton表示的内存引用地址不可变):

    public class Singleton {
        // 不能用final修饰,否则编译不过
        private static Singleton singleton = new Singleton();
    
        private Singleton(){}
    
        public static Singleton getInstance() {
            return singleton;
        }
    
        public static void releaseInstance() {
            if (singleton != null) {
                singleton = null;
            }
        }
    }
    

2.懒汉模式

public class Singleton {
    private static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            return new Singleton();
        }
        return singleton;
    }
}
  • 1.优点:用的时候再去创建,不浪费空间
  • 2.缺点:线程不安全

2.1线程安全的懒汉式单例模式代码1

```java
public static synchronized Singleton getInstance() {
    if (singleton == null) {
        return new Singleton();
    }
    return singleton;
}
```
缺点:如果多个线程同时执行,那么每次执行`getInstance()`方法时每个线程都要先尝试获得锁再去执行方法体,若该线程没有获得锁,那么该线程就要等待,耗时很长。

2.2线程安全的懒汉式单例模式代码2

```java
public static Singleton getInstance(){
    if (singleton == null) {
        synchronized (Singleton.class){
            singleton = new Singleton();
        }
    }
    return singleton;
}
```
缺点:线程A和线程B,线程A读取`singleton`值为`null`,此时cpu被线程B抢去了,线程B再来判断`singleton`值为null,于是,它开始执行同步代码块中的代码,对`singleton`进行实例化。此时,线程A获得cpu,由于线程A之前已经判断`singleton`值为`null`,于是开始执行它后面的同步代码块代码。它也会去对`singleton`进行实例化。这样就会导致创建了两个不一样的实例。

2.3线程安全的懒汉式单例模式代码3

(双重检查加锁,volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,不建议使用)

“双重检查加锁”机制的实现会使用关键字`volatile`,它的意思是:被`volatile`修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。(被`volatile`修饰的`value`,某线程修改`value`的值时,其他线程看到的`value`值都是最新的`value`值)

**那么“双重检查加锁”机制的实现为什么要使用关键字`volatile`?**
需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。

instance = new Singleton();

可以分解为3行伪代码

a.memory = allocate() //分配内存

b. ctorInstanc(memory) //初始化对象

c. instance = memory //设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。

在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,

B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,

即先执行了a和c,没有执行b。

那么由于A线程执行了c导致instance指向了一段地址,

所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。
```java
public class Singleton {
    // 用volatile修饰静态成员变量
    private volatile static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance(){ //1
        // 首先检查实例是否存在
        if (singleton == null) { //2
            // 同步代码块,保证线程安全的创建实例
            synchronized (Singleton.class){ //3
                // 再次检查实例是否存在
                if (singleton == null) { //4
                    singleton = new Singleton(); //5
                }
            }
        }
        return singleton;
    }
}
```

3.静态内部类单例模式

public class Singleton {
    // 静态内部类
    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }

    private Singleton() {}

    private static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
}
  • 1.为什么静态内部类单例模式线程安全

    静态内部类不会随着外部类的加载而加载,只有静态内部类的静态成员被调用时才会进行加载 , 这样既保证的惰性初始化(Lazy-Initialazation),又由JVM保证了多线程并发访问的正确性。请点击:类加载过程的线程安全性保证

4.枚举单例模式

public enum SingletonEnum {
    INSTANCE;

    public void doSomething() {
        System.out.println("hello singleton");
    }

    public static void main(String[] args) {
        SingletonEnum.INSTANCE.doSomething();
    }
}
  • 1.利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。

5.基于CAS的单例模式

什么是CAS

不需要使用synchronized和lock即可保证安全性,代码如下:

public class SingletonCas {
    private static final AtomicReference<SingletonCas> INSTANCE = new AtomicReference<>();

    private SingletonCas() {
    }

    /**
     * 用cas确保安全性
     */
    public SingletonCas getInstance() {
        for (;;) {
            SingletonCas current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new SingletonCas();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }
}

拓展

类加载过程的线程安全性保证

以上的静态内部类、饿汉等模式均是通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。

这其实是利用了ClassLoader的线程安全机制。ClassLoaderloadClass方法在加载类的时候使用了synchronized关键字。

所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。

枚举的特性

枚举类型是线程安全的,并且只会装载一次。

使用javap -p SingletonEnum.class反编译class文件,得到如下代码

public final class com.demo.thymeleafstudy.singleton.SingletonEnum extends java.lang.Enum<com.demo.thymeleafstudy.singleton.SingletonEnum> {
  public static final com.demo.thymeleafstudy.singleton.SingletonEnum INSTANCE;
  
  private static final com.demo.thymeleafstudy.singleton.SingletonEnum[] $VALUES;
  
  public static com.demo.thymeleafstudy.singleton.SingletonEnum[] values();
  
  public static com.demo.thymeleafstudy.singleton.SingletonEnum valueOf(java.lang.String);
  
  private com.demo.thymeleafstudy.singleton.SingletonEnum();
  
  public void doSomething();
  
  public static void main(java.lang.String[]);
  
  static {};
}

从反编译结果可知:

  • 枚举类型的关键字enum,其实只是一个语法糖,编译器最终把它转化为一个final类,因此枚举是不可继承的。

  • 枚举类型都继承自 java.lang.Enum 类。

  • 枚举的每一个取值被编译器传化为了一个个static final 属性,因为static类型的属性会在类被加载之后被初始化。(类似于饿汉单例模式)。

破坏单例模式的方法以及如何避免

1.通过反射破坏单例

除枚举方式外, 其他方法都可以通过反射的方式破坏单例。反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法如下:

private Singleton() {
    if (instance !=null) {
        throw new RuntimeException("实例已经存在,请通过getInstance()方法获取");
    }
}

2.通过反序列化破坏单例

如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例。验证如下(SerializeUtil工具类代码参考这个博客点我):

public class Singleton implements Serializable {
    private static final long serialVersionUID = 1580706254912463448L;

    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }

    private Singleton() {}

    private static Singleton getInstance() {
        return SingletonHolder.singleton;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 获取单例对象
        Singleton singleton1 = Singleton.getInstance();
        // 序列化
        SerializeUtil.serialize(singleton1, "D://test_singleton.txt");
        // 反序列化
        Singleton singleton2 = (Singleton) SerializeUtil.deserialize("D://test_singleton.txt");
        // 验证两个对象是否相等
        System.out.println(singleton1 == singleton2);
    }
}

输出结果如下:

false

因此得出结论,反序列化后的单例对象和原单例对象并不相等了。

解决办法是如果一个单例类必须要实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回单例对象。

public Object readResolve() throws ObjectStreamException {
    return instance;
}

CAS

什么是CAS

cas是compareandswap的简称,从字面上理解就是比较并更新(或者说比较并交换,用于保证并发时的无锁并发的安全性)。

简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。原文出处

CAS能够做什么

它可以解决多线程并发安全的问题,以前我们对一些多线程操作的代码都是使用synchronize关键字,来保证线程安全的问题。

现在我们将cas放入到多线程环境里我们看一下它是怎么解决的。我们假设有A、B两个线程同时执行一个int值value自增的代码,并且同时获取了当前的value。假设线程B比A快了那么0.00000001s,所以B先执行,线程B执行了cas操作之后,发现当前值和预期值相符,就执行了自增操作,此时这个value = value + 1;然后A开始执行,A也执行了cas操作,但是此时value的值和它当时取到的值已经不一样了。所以此次操作失败,重新取值然后比较成功,然后将value值更新,这样两个线程进入,value值自增了两次,符合我们的预期。原文出处

AtomicReference(提供以无锁方式访问共享资源的能力)

点我查询查看更详细说明

AtomicReference实例持有一个对象(这个对象就是共享资源)的引用,initialValue,用volatile修饰,并通过unsafe类来操作该引用。

public class AtomicReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        AtomicReference<Integer> ref = new AtomicReference<>(new Integer(1000));

        for (int i = 0; i < 1000; i++) {
            Thread t = new Thread(new Task(ref), "Thread-"+i);
            t.start();
        }

        // 保证前面线程全部执行完
        Thread.sleep(10000);
        System.out.println("####"+ref.get());
    }
}

public class Task implements Runnable {
    AtomicReference<Integer> ref;

    public Task(AtomicReference<Integer> ref) {
        System.out.println("construct="+ref.get());
        this.ref = ref;
    }

    @Override
    public void run() {
        // for(;;)表示死循环,也就是自旋
        for(;;) {
            Integer oldV=ref.get();
            System.out.println(Thread.currentThread().getName()+":"+oldV);
            /**
             * compareAndSet方法会将入参的expect变量所指向的对象和AtomicReference中的引用对象进行比较,
             * 如果两者指向同一个对象,
             * 则将AtomicReference中的引用对象重新置为update,修改成功返回true,失败则返回false。
             * 也就是说,AtomicReference其实是比较对象的引用。
             */
            if (ref.compareAndSet(oldV, oldV+1)) {
                break;
            }
        }
    }
}

下面看一下compareAndSet方法

/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that
 * the actual value was not equal to the expected value.
 */
public final boolean compareAndSet(V expect, V update) {
    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapObject
(Object var1, long var2, Object var4, Object var5);

最终compareAndSet方法调用了UnSafe类的compareAndSwapObject方法(比较并交换)。

1.此方法是Java的native方法,并不由Java语言实现。

2.方法的作用是,读取传入对象var1在内存中偏移量为var2位置的值与期望值var4作比较。如果相等就把var5值赋值给var2位置的值。方法返回true。不相等,就取消赋值,方法返回false。