Java JUC并发之单例模式的巧妙用法
十八、单例模式
程序员必会!!!
1、饿汉式
// 饿汉式单例
public class Hungry {
// 在饿汉式单例下 这些资源一起全部加载进来
// 会造成空间浪费
private byte[] data1 = new byte[1024];
private byte[] data2 = new byte[1024];
private byte[] data3 = new byte[1024];
private byte[] data4 = new byte[1024];
private Hungry() { // 构造器私有
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance() {
return HUNGRY;
}
}
饿汉式与懒汉式的本质区别:
-
饿汉式是 线程安全 的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变;懒汉式 如果在创建实例对象时不加上synchronized,则会导致对对象的访问不是 线程安全 的。
-
从实现方式来讲:懒汉式是延时加载,只有在需要的时候才创建对象;
而饿汉式在虚拟机启动的时候就会创建,饿汉式无需关注多线程问题、写法简单明了、能用则用。但是它是加载类时创建实例、所以如果是一个 工厂模式 ,缓存了很多实例、那么就得考虑效率问题,因为这个类一加载,则把所有实例不管用不用一块创建。
2、懒汉式
双重检查锁(Double Checked Locking)
在单线程情况下,实现单例模式是安全的,但是如果考虑多线程,就可能会出现问题,导致出现多个LazyMan的实例!
原因:考虑可能有两个线程同时调用getInstance(),LazyMan就会被实例化两次 并且被不同对象持有,完全违背单例模式的初衷。
解决方法一 加锁 :
// 懒汉式单例-加锁
public class LazyMan {
private LazyMan() {
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
加锁虽然能解决问题,但是因为用到了synchronized,会导致很大的性能开销,并且每次初始化时都会加锁,性能浪费。
解决方法二、双重检查锁(有缺陷)
先判断对象是否已被初始化,再决定要不要加锁
// 懒汉式单例-加锁
public class LazyMan {
private LazyMan() {
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
}
return lazyMan;
}
}
这样写的话,运行顺序就会变成:
- 检查变量是否被初始化(先不去获得锁),如果已被初始化则立即返回
- 获得锁
- 再次检查变量是否已被初始化,如果还没被初始化,就初始化一个对象
执行双重检查是因为:如果多个线程同时通过了第一次检查,并且其中一个线程首先通过第二次检查并实例化了对象,其余通过了第一次检查的线程就不会再去实例化对象。
加双重检查的好处:除了初始化的时候会加锁,后续的所有调用都会避免枷锁,直接返回,解决了性能消耗的问题!
DCL 懒汉式仍然存在缺陷!
lazyMan = new LazyMan();
实例化上述对象的过程并不是原子性操作,它可以分为三步(编译器层面):
- 分配内存空间
- 初始化对象,执行构造方法
- 将对象指向刚才分配的内存空间
但是底层的编译器为了性能,可能会对这三步操作进行重排序(指令重排)
执行顺序可能会变成 1-3-2
假设A线程先通过第一次判断,获得锁,并且A线程调用顺序为1-3-2,执行完3之后,B线程刚好检查到 lazyMan 不为空,便返回一个未初始化完成的对象,于是产生了两个实例!
完整的DLC懒汉式 => 对lazyMan 加上 volatile 关键字 禁止指令重排
package com.liu.single;
// 懒汉式单例
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "LazyMan ok");
}
private volatile static LazyMan lazyMan; // volatile 禁止指令重排
public static LazyMan getInstance() {
// 双重检测锁模式 懒汉式单例 DCL (Double Check Locking)
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan(); // new 关键字 非原子性操作 可能会出现指令重排
}
}
}
return lazyMan;
}
public static void main(String[] args) {
// 多线程并发
// 不安全
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
小结 : 单例模式下使用volatile,可以禁止指令重排!
DLC + volatile 才是完整的懒汉式单例
静态内部类的懒汉式
package com.liu.single;
// 使用静态内部类
public class Holder {
private Holder() {
}
private static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
以上的单例都不安全 = > 因为有反射的存在,可以轻松破坏单例的安全性
package com.liu.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
// 懒汉式单例
public class LazyMan02 {
private static boolean liu = false; // 关键字可以加密 防止反射破坏
private LazyMan02() {
synchronized (LazyMan02.class) {
if (liu == false) {
liu = true;
}else {
throw new RuntimeException("不要试图用反射破坏代码!");
}
}
//System.out.println(Thread.currentThread().getName() + "LazyMan ok");
}
private volatile static LazyMan02 lazyMan; // volatile 禁止指令重排
public static LazyMan02 getInstance() {
// 双重检测锁模式 懒汉式单例 DCL (Double Check Locking)
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan02(); // new 关键字 非原子性操作 可能会出现指令重排
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
// 反射方法 :getDeclaredFiekd() 获得该类中声明的所有字段
Field liu = LazyMan02.class.getDeclaredField("liu");
// 反射 => 可以破坏单例
//LazyMan02 instance = LazyMan02.getInstance();
Constructor<LazyMan02> declaredConstructor = LazyMan02.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan02 instance = declaredConstructor.newInstance();
liu.set(instance, false); // 反射可以破坏单例的安全性
LazyMan02 instance02 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance02);
}
}
使用枚举类可以防止反射的破坏
package com.liu.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Enum 枚举
* 本身也是一个类
*/
public enum EnumSingle {
INSTANCE;
private EnumSingle() {
}
public EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
EnumSingle instance = EnumSingle.INSTANCE;
//java.lang.NoSuchMethodException:
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
通过IDEA查看枚举类底层源码 => 无参构造器,使用反射之后,发现报的错误并不是我们预想的结果
使用无参构造的Enum:报错 => java.lang.NoSuchMethodException
预期报错: java.lang.IllegalArgumentException: Cannot reflectively create enum objects
Java反编译工具Jad 下载 :https://www.jianshu.com/p/5d8736d9a32a
使用命令javap -p EnumSingle.class进行编译:
可以看到编译之后的.java文件里面也是使用无参构造!
枚举类型的最终反编译源码如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingle.java
package com.liu.single;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/liu/single/EnumSingle, name);
}
private EnumSingle(String s, int i)
{
super(s, i); // 证实枚举类使用的是有参构造,参数分别是String和int
}
public EnumSingle getInstance()
{
return INSTANCE;
}
public static void main(String args[])
throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException
{
EnumSingle instance = INSTANCE;
Constructor declaredConstructor = com/liu/single/EnumSingle.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = (EnumSingle)declaredConstructor.newInstance(new Object[0]);
System.out.println(instance);
System.out.println(instance2);
}
private static EnumSingle[] $values()
{
return (new EnumSingle[] {
INSTANCE
});
}
public static final EnumSingle INSTANCE = new EnumSingle("INSTANCE", 0);
private static final EnumSingle $VALUES[] = $values();
}
将Enum类中通过反射传入两个参数作为构造器的参数 : String.class int.class
package com.liu.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum EnumSingle {
INSTANCE;
private EnumSingle() {
}
public EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
EnumSingle instance = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class); // 使用String.class 和 int.class作为构造器的参数
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
其报错结果为:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
即:无法通过反射来创建枚举类的实例 => 单例模式下使用枚举enum可以有效阻止反射,保证单例模式的安全性不被破坏!