1. 静态常量
饿汉式单例,这种情况优点是简单,缺点是加载的太早了,在加载这个类的时候就被实例化出来了
/**
* 饿汉式单例
* 缺点:一装载就实例化了,所以不用他,干嘛加载
* 所以就有了懒汉式加载
*/
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return instance;
}
}
2. 静态语句块
使用静态语句块,和1差不多,也是饿汉式的加载,在类被加载时就被实例化了
/**
* 使用静态语句块
* 和饿汉式差不多
*/
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance() {
return instance;
}
}
3. 方法中判断(有问题)
这种方式实现了懒加载,在需要的时候才实例化。缺点是有线程安全问题,在多线程访问时,会被多次实例化
/**
* 懒加载
* 懒汉式
* 缺点:有线程安全问题
*/
public class Singleton {
private static Singleton INSTANCE;
private Singleton(){}
public static Singleton getInstance() {
if (INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
4. 方法中判断
这种方式在3上面做了改进,解决了线程安全问题,同时也是懒加载。通过加锁来解决了线程安全的问题,缺点就是锁的粒度太大了,处理在第一次实例化的时候需要加锁保证线程安全,后面就不需要的,因为已经实例化出来了,直接取就行了。
/**
* 懒加载
* 懒汉式加载,使用锁来保证线程安全
* 缺点:锁的粒度太大,instance有值之后可以不需要再加锁了
*/
public class Singleton {
private static Singleton instance;
private Singleton(){}
public synchronized static Singleton getInstance() {
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
5. 减小锁粒度(有问题)
这种方式也是在3上面做了改进,但是看上去可能不会有问题,但是在多线程的情况下,多个线程进入了instance == null这个判断之后,会阻塞在sync之前,当一个线程结束进行实例化之后,后一个线程也会进行实例化,就会产生多个实例出来。
/**
* 懒加载
* 懒汉式加载,使用锁来保证线程安全
* 缺点:有线程安全问题
*/
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null){
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
6. 双重检查(有问题)
这种情况看上去也是正常的,但是也是有线程安全问题的。问题出在new Singleton();这一步,在这一步会产生指令重排序问题。
/**
* 懒加载
* 懒汉式加载,使用锁来保证线程安全
*/
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
//第一次可以保证在初始化完成之后可以不进入锁
if (instance == null){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
6.1 new Singleton();的指令重排序
指令重排序是有JVM内部和CPU执行的一个优化代码执行的方式,将没有数据依赖的语句可以执行重排序,可以由CPU的多个核心乱序执行。这样可以保证代码执行的效率,只保证了最终一致性,保证代码最后执行的结果是一致的。
那么在new Singleton();
语句中我们查看jvm字节码的话可以看到实际上是有多句的。
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return
实际上可能分为了三部分,第一部分是在内存中开辟空间,对象做初始化工作;第二部分是赋初值完成对象初始化工作;第三部分是将内存中的那个空间的地址赋给字段。那么在这三部分中,第二部分和第三部分是没有数据依赖的,是有可能会出现重排序的。那么如果在第三步先执行的情况下,这个时候我们的instance已经有值了,如果第二个线程来获取,就会直接返回,但是对象的初值还没有附上去,就会出现数据错误,所有的值都是初值的情况
7. 双重检查
在这种方式下,使用了volatile关键字来防止指令重排序
/**
* 懒加载
* 懒汉式加载,使用锁来保证线程安全
* 双重检测,可以保证线程安全,也可以保证懒加载
*/
public class Singleton {
//添加volatile防止指令重排序
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
8. 静态内部类
这种方式既保证了线程安全,也保证了懒加载
/**
* 懒加载
* 懒汉式加载
* 静态内部类
*/
public class Singleton {
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
上方所有方式看上去都没有问题,但是还是有一点小小的问题,就是可以被通过反射修改权限,然后执行构造方法,生成另一个实例。但是一般没人会这么干。
9. 枚举类
使用这种方式可以完美的避开上述的所有问题。这种方式也是在《Effective Java》中推荐的方式。
但是我个人感觉这种方式不太好,就是枚举类型和我们正常使用的类型还是不太一样的。
/**
* 防止反序列化
*/
public enum Singleton {
instance;
}
枚举类之所以无法通过反射实例化,是因为在java.lang.reflect.Constructor#newInstance
中
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//在这里作出了判断如果为enumerate类型的话,就会抛出异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
所以如果有需要使用单例的情况,就选择第一种就行了,如果有懒加载的方式,可以选择第七种或者第八种。