单例模式保证一个类只有一个实例,并且提供一个访问它的全局访问点。
主要防止一个全局使用的对象频繁的创建和销毁;并且可以控制实例数目,节省了系统资源;还可以避免对资源的多重占用。
缺点是没有接口,不能继承。
最主要的构建思路就是将构造函数设置为私有。
方法一:懒汉式
public class LHan {
private static LHan instance;
private LHan() {}
public static LHan getInstance() {
if (instance == null) {
instance = new LHan();
}
}
}
优点是不加锁就可以获取到对象实例,线程安全。
主要缺点是类一装载进来就会创建对象,稍微存在内存的浪费。
方法二:饿汉式
public class EHan {
private static EHan instance = new EHan();
private EHan() {}
public static EHangetInstance() {
return instance;
}
}
优点是解决了饿汉式浪费内存的问题,在真正需要的时候再去执行初始化。
但是在多线程的情况下可能会出现同步问题。解决方法就是使用双检锁。
方法三:双检锁
public class DoubleCheck {
private volatile static DoubleCheck instance;
private DoubleCheck() {}
public static DoubleCheck getInstance() {
if (instance == null) {
synchronized(DoubleCheck.class) {
if (instance == null) {
instance = new DoubleCheck();
}
}
}
}
}
关于如何得出来双检锁这样的写法可以看文章的下一部分:双检锁由来。
方法四:静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
Singleton类加载的时候,SingletonHolder不会加载,只有在调用getInstance方法的时候才会执行初始化,这样既起到了懒加载的作用,同时又使用到了JVM类加载机制,保证了单例对象初始化的线程安全。
方法五:枚举类
public enum Singleton {
INSTANCE;
public void anyMethod() {
}
}
利用枚举类可以非常简洁的实现单例模式。
实现单例模式时如果未考虑多线程,很容易写出以下代码:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public static Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
上面代码的问题在于多线程时可能会导致产生多个实例。解决的方法是加锁:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
这样虽然解决了可能产生多个实例的问题,却可能产生很大的性能开销。并且加锁其实只要第一次调用时用到,之后的调用都不需要加锁。所以就有下面的优化写法,先来一个错误的例子:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public static Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton(); // error
}
}
}
return uniqueSingleton;
}
}
上面的运行顺序是:
执行双重检查是因为,如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。
这样,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题。
上面的写法看似解决了问题,但其实有很大的隐患。实例化对象的代码可以分为以下三个步骤:
但是有些编译器为了优化性能,可能会将第二步和第三步进行重排序,如果在一个线程已经将指针指向了分配的内存空间却还没有初始化对象,这时另一个线程获取实例,这个线程就会获得一个还未初始化的实例。
解决方法是使用volatile关键字修饰实例。
public class Singleton {
private volatile static Singleton uniqueSingleton;
private Singleton() {
}
public static Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
使用volatile之后重排序被禁止,所有的写操作都发生在读操作之前。volatile关键字的具体用法可以查看参考链接。
除了枚举之外,其他的单例模式写法都可以通过反射和序列化的方法来破坏。
具体可以查看第二个参考链接。
参考链接:
Java中的双重检查锁(double checked locking)(文中的代码缺少static关键字)
一次群聊引发的血案
Java并发编程:volatile关键字解析
原文:https://www.cnblogs.com/haruhiui/p/14669792.html