关于设计模式:线程安全的几种单例模式

3次阅读

共计 3663 个字符,预计需要花费 10 分钟才能阅读完成。

单例模式
单例模式是 Java 中罕用的设计模式之一,属于设计模式三大类中的创立型模式。在运行期间,保障某个类仅有一个实例,并提供一个拜访它的全局拜访点。单例模式所属类的构造方法是公有的,所以单例类是不能被继承的。实现线程平安的单例模式有以下几种形式:
1. 饿汉式

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

这是实现一个平安的单例模式的最简略粗犷的写法,之所以称之为饿汉式,是因为肚子饿了,想马上吃到货色,不想期待生产工夫。在类被加载的时候就把 Singleton 实例给创立进去供应用,当前不再扭转。

长处:实现简略,线程平安,调用效率高(无锁,且对象在类加载时就已创立,可间接应用)。

毛病:可能在还不须要此实例的时候就曾经把实例创立进去了,不能延时加载(在须要的时候才创建对象)。

2. 懒汉式

public class Singleton {
    
    private static Singleton instance = null;
 
    private Singleton() {}
 
    // 如果没有 synchronized,则线程不平安
    public static synchronized Singleton getInstance() {//synchronized 也能够写在办法里,造成同步代码块
        if (instance == null) {instance = new Singleton();
        }
        return instance;
    }
    
}

相比饿汉式,懒汉式显得没那么“饿”,在真正须要的时候再去创立实例。

长处:线程平安,能够延时加载。

毛病:调用效率不高(有锁,且须要先创建对象)。

3. 懒汉式改良版(双重同步锁)

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

应用了 double-check,即 check- 加锁 -check,缩小了同步的开销

第 2 种懒汉式的效率低在哪里呢?第二种写法将 synchronized 加在了办法上(或者写在办法里),在单例对象被创立后,因为办法加了锁,所以要等以后线程失去对象开释锁后,下一个线程才能够进入 getInstance()办法获取对象,也就是线程要一个一个的去获取对象。而采纳双重同步锁,在 synchronized 代码块前加了一层判断,这使得在对象被创立之后,多线程不需进入 synchronized 代码块中,能够多线程同时并发拜访获取对象,这样效率大大提高。

在创立第一个对象时候,可能会有线程 1,线程 2 两个线程进入 getInstance()办法,这时对象还未被创立,所以都通过第一层 check。接下来的 synchronized 锁只有一个线程能够进入,假如线程 1 进入,线程 2 期待。线程 1 进入后,因为对象还未被创立,所以通过第二层 check 并创立好对象,因为对象 singleton 是被 volatile 润饰的,所以在对 singleton 批改后会立刻将 singleton 的值从其工作内存刷回到主内存以保障其它线程的可见性。线程 1 完结后线程 2 进入 synchronized 代码块,因为线程 1 曾经创立好对象并将对象值刷回到主内存,所以这时线程 2 看到的 singleton 对象不再为空,因而通过第二层 check,最初获取到对象。这里 volatile 的作用是保障可见性,同时也禁止指令重排序,因为上述代码中存在管制依赖,多线程中对管制依赖进行指令重排序会导致线程不平安。

长处:线程平安,能够延时加载,调用效率比 2 高。

4. 外部动态类
public class Singleton {

private Singleton() {}

public static Singleton getInstance() {return SingletonFactory.instance;}

private static class SingletonFactory {private static Singleton instance = new Singleton();
}

}
动态外部类只有被被动调用的时候,JVM 才会去加载这个动态外部类。外部类首次加载,会初始化动态变量、动态代码块、静态方法,但不会加载外部类和动态外部类。

长处:线程平安,调用效率高,能够延时加载。

仿佛动态外部类看起来曾经是最完满的办法了,其实不是,可能还存在反射攻打和反序列化攻打。

a)反射攻打

public static void main(String[] args) throws Exception {Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();
    System.out.println(singleton == newSingleton);
}

运行后果:false

通过后果看,这两个实例不是同一个,违反了单例模式的准则。

b)反序列化攻打

引入依赖:


<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>

这个依赖提供了序列化和反序列化工具类。

Singleton 类实现 java.io.Serializable 接口。

public class Singleton implements Serializable {
 
    private static class SingletonHolder {private static Singleton instance = new Singleton();
    }
 
    private Singleton() {}
 
    public static Singleton getInstance() {return SingletonHolder.instance;}
 
    public static void main(String[] args) {Singleton instance = Singleton.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton newInstance = SerializationUtils.deserialize(serialize);
        System.out.println(instance == newInstance);
    }
 
}

运行后果:false

5. 枚举
最佳的单例实现模式就是枚举模式。写法简略,线程平安,调用效率高,能够人造的避免反射和反序列化调用,不能延时加载。
public enum Singleton {

INSTANCE;

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

}
调用办法:

public class Main {public static void main(String[] args) {Singleton.INSTANCE.doSomething();
    }
 
}

间接通过 Singleton.INSTANCE.doSomething()的形式调用即可。

枚举如何实现线程平安?反编译后能够发现,会通过一个类去继承改枚举,而后通过动态代码块的形式在类加载时实例化对象,与饿汉式相似。https://blog.csdn.net/wufalia…

如何做到避免反序列化调用?每一个枚举类型及其定义的枚举变量在 JVM 中都是惟一的,Java 做了非凡的规定,枚举类型序列化和反序列化进去的是同一个对象。

除此之外,枚举还能够避免反射调用。

** 综上,线程平安的几种单例模式比拟来看:
枚举(无锁,调用效率高,能够避免反射和反序列化调用,不能延时加载)> 动态外部类(无锁,调用效率高,能够延时加载)> 双重同步锁(有锁,调用效率高于懒汉式,能够延时加载)> 懒汉式(有锁,调用效率不高,能够延时加载)≈ 饿汉式(无锁,调用效率高,不能延时加载)

ps: 只有枚举能避免反射和反序列化调用 **

正文完
 0