线程安全的⼏种单例模式
单例模式
单例模式是 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 = Instance();
Constructor<Singleton> constructor = DeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = wInstance();
System.out.println(singleton == newSingleton);
}
运⾏结果:false
通过结果看,这两个实例不是同⼀个,违背了单例模式的原则。
b)反序列化攻击
引⼊依赖:
<dependency>单例模式的几种实现方式
<groupId>org.apachemons</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 = Instance();
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()的⽅式调⽤即可。
如何做到防⽌反序列化调⽤?每⼀个枚举类型及其定义的枚举变量在JVM中都是唯⼀的,Java做了特殊的规定,枚举类型序列化和反序列化出来的是同⼀个对象。
除此之外,枚举还可以防⽌反射调⽤。
综上,线程安全的⼏种单例模式⽐较来看:
枚举(⽆锁,调⽤效率⾼,可以防⽌反射和反序列化调⽤,不能延时加载)> 静态内部类(⽆锁,调⽤效率⾼,可以延时加载) > 双重同步锁(有锁,调⽤效率⾼于懒汉式,可以延时加载) > 懒汉式(有锁,调⽤效率不⾼,可以延时加载) ≈ 饿汉式(⽆锁,调⽤效率⾼,不能延时加载)
ps:只有枚举能防⽌反射和反序列化调⽤
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论