java singleton

ddatsh

dev #java

适合单线程程序,多线程需要保护getInstance(),否则可能产生多个Singleton对象实例

public class Singleton {

    private Singleton() {
    }

    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (Singleton.instance == null) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

getInstance()一次只能被一个线程调,锁定整个方法,低效,99%不需要同步

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (Singleton.instance == null) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

实际会产生多线程访问问题的只有

instance = new Singleton(); 

为降低 synchronized 块性能方面影响,如果只锁定这行?

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (Singleton.instance == null) {
            synchronized (Singleton.class) {
                Singleton.instance = new Singleton();
            }
        }
        return Singleton.instance;
    }
}

当两个线程并发进入第一次判断instance是否为空的if 语句内部

第一个线程执行new操作,第二个线程阻断,当第一个线程执行完毕之后,第二个线程没有进行判断就直接进行new操作,多实例化了,所以这样也不是安全的

为了避免第二次进入synchronized块没有进行非空判断的情况发生,添加第二次条件判断,引入了在java中更诡异的 double check问题

Effecitve Java 指出在 Java 中双重检查模式通常并不适用

public class Singleton {

	private static Singleton instance = null;

	private Singleton() {

	}

	public static Singleton getInstance() {

		if (instance == null){                               	   //1
			synchronized (Singleton.class) {  	  	//2
				if (instance == null) {                           //3
					instance = new Singleton(); 	    //4
				}
			}
		}
		return instance;
	}
}

可见性问题?

线程1执行完第4步,释放锁

线程2获得锁后执行到第3步,由于可见性的原因,发现instance还是null,从而初始化了两次?

但不会存在这种情况,因为synchronized能保证线程1在释放锁之前会将对变量的修改刷新到主存当中,线程2拿到的值是最新的

实际存在的问题是无序性

第4步这个new操作是无序的,它可能会被编译成:

然而需要意识到synchronized虽然是互斥的,但不代表一次就把整个过程执行完,它在中间是可能释放时间片的,时间片不是锁(这里没转过来,会耽误很多时间)

也就是说可能在a执行完后,时间片被释放,线程2执行到1,这时读到的instance是不是null呢?(标记1)

基于可见性,可能是null,也可能不是null

非常奇葩的是,这例子中,如果读到的是null,反而没问题了,接下来会等待锁,然后再次判断时不为null,最后返回单例

如果读到的不是null,那么坏了,按逻辑它就直接return instance了,这个instance还没执行构造参数,去做事情的话,很可能就崩溃了

volatile

public class Singleton {

	private volatile static Singleton instance = null;

	private Singleton() {

	}

	public static Singleton getInstance() {

		if (instance == null){                                  //1
			synchronized (Singleton.class) {    //2
				if (instance == null)                           //3
					instance = new Singleton();     //4
			}
		}
		return instance;
	}

}

jdk1.4及之前,volatile不能保证new操作的有序性,但是它能保证可见性,因此标记1处,读到的不是null,导致了问题

1.5 volatile关键字,初始化就不能是:

而应该是:

这种形式,也就避免了无序性问题

在 Java 中双重检查模式无效的原因是在不同步的情况下引用类型不是线程安全的

除了 long 和 double 的基本类型,双重检查模式是适用的

下面这段代码就是正确的

private int count;  
public int getCount(){  
  if (count == 0){   
    synchronized(this){   
      if (count == 0){  
        count = computeCount();  //一个耗时的计算  
      }     
    }    
  }  
  return count;  
}  

双重检查锁定失败的问题并不归咎于 jvm 实现 bug,而是 jvm 内存模型允许“无序写入会导致二次检查失败

懒汉式的lazy方式实现单例弯弯绕太多,单线程编程的情况下懒汉式单例实现没有任何问题

2. no synchronized ,用 static

public class Singleton {

	private static Singleton instance = new Singleton();
	private Singleton() {
	}

    public static Singleton getInstance() {
        return Singleton.instance;
    }
}

更加灵巧,没有使用同步但保证了只有一个实例,还同时具有了Lazy的特性

Initialization on Demand Holder(IODH)

public class Singleton {  
    static class SingletonHolder {  
        static Singleton instance = new Singleton();  
    }  
      
    public static Singleton getInstance(){  
        return SingletonHolder.instance;  
    }  
}  

加入私有静态内部类,内部类直到他们被引用时才会加载

且一个类里面static的成员变量只会有一份,保证多个线程同时访问,所拿到的Resource对象都是同一个

饿汉式的实现方式虽然貌似开销比较大,但是不会出现线程安全的问题,也是解决线程安全的单例实现的有效方式

至于ThreadLocal,还是应该由使用场景来决定

在《Java与模式》中,作者提出:“饿汉式单例类可以在Java语言实现,但不易在C++内实现,因为静态初始化在C++里没有固定的顺序,因而静态的instance变量的初始化与类的加载顺序没有保证,可能会出问题

这就是为什么GoF在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java语言中单例类的例子也大多是懒汉式的

实际上,饿汉式单例类更符合Java语言本身的特点

在应用设计模式的同时,分析具体的使用场景来选择合适的实现方式是非常必要的

jvm 内存模型改进

Doug Lea 写道:“JSR133 Java 内存模型,将引用类型声明为 volatile,双重检查模式就可以工作了

1.4 前 out-of-order writes 不可用, 1.5 可用,即可保证多线程下的单例

private volatile Resource resource;  
public Resource getResource(){  
  if (resource == null){   
    synchronized(this){   
      if (resource==null){  
        resource = new Resource();    
      }     
    }    
  }  
  return resource;  
}  

还有种用Final的方法

https://zh.wikipedia.org/wiki/双重检查锁定模式

用枚举

private static enum EnumSingleton{  
        INSTANCE;  
          
        private Singleton singleton;  
          
        //JVM会保证此方法绝对只调用一次  
        private EnumSingleton(){  
            singleton = new Singleton();  
        }  
          
        public Singleton getInstance(){  
            return singleton;  
        }  
    }