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

单例模式
单例模式是 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:只有枚举能避免反射和反序列化调用**

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理