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