单例模式是一种常用的软件设计模式,常被用于一个系统中一个类只存在一个实例的场合,从而方便对实例个数的控制并节约系统资源。”简而言之,单例模式就是保证一个类最多只能存在一个实例对象。
使用场景:
1)对应于频繁使用的对象,可以省略创建对象所花费的时间,对于一些大型对象,是一笔可观的开销。
2)由于减少了new对象,减轻系统内存使用频率和GC开销。
单例模式必须要有private访问权限的构造函数,确保该类不会被其他代码实例化,其次是instance成员变量和getInstance方法必须是static,在实现方式上可以分成:懒汉模式和饿汉模式
懒汉模式:
顾名思义,lazy loading(延迟加载,一说懒加载),在需要的时候才创建单例对象,而不是随着软件系统的运行或者当类被加载器加载的时候就创建。当单例类的创建或者单例对象的存在会消耗比较多的资源,常常采用lazy loading策略。这样做的一个明显好处是提高了软件系统的效率,节约内存资源。下面我们看看最简单的懒汉单例模式:
public class Singleton {private static Singleton instance = null ;
private Singleton() {
System. out .print( "private construct func..." );
}
public static Singleton getInstance() {
if ( instance == null ) {
instance = new Singleton();
}
return instance ;
}
}
饿汉模式:相对于懒汉模式,饿汉模式也有其不足之处,在类被加载时就创建单例对象并且长驻内存,不管你是否使用到它;如果Singleton类占用的资源比较多,势必会降低资源利用率以及程序运行效率。
public class Singleton {private static Singleton instance =new Singleton();
private Singleton() {
System.out.print("private construct func...");
}
public static Singleton getInstance() {
return instance;
}
}
在单线程环境下,多次调用getInstance()方法获得的Singleton对象均为同一个对象。然而,实际应用环境中很多都会涉及到多线程,因此不得不考虑线程安全的问题。简单的懒汉式在多线程情况下,可能出现多个线程同时调用getInstance方法,此时都判断instance为null,每个线程都会创建多个Singleton实例,违背了单例模式的初衷。这说明懒汉式在多线程环境下不是线程安全的。于是想到在getInstance()方法上同步锁。
public class Singleton {private static Singleton instance =null;
private Singleton() {
System.out.print("private construct func...");
}
public static synchronized Singleton getInstance() {
if (instance ==null) {
instance = new Singleton();
}
return instance;
}
}
虽然这种在方法前加上同步锁的方式可以解决多线程问题,但是在使用中通常只需在第一次创建对象,以后调用getInstance方法只需要返回已经实例化的Singleton对象即可,但是因为同步锁锁住整个方法可能粒度过大,不利于效率。既然锁方法不太好,那么锁代码呢?
public class Singleton {private static Singleton instance =null;
private static Object lock =new Object();
private Singleton() {
System.out.print("private construct func...");
}
public static Singleton getInstance() {
if (instance ==null) {
synchronized (lock) {
instance = new Singleton();
}
}
return instance;
}
}
在getInstance()方法里,在判空语句后上锁,判空处主要是为了性能考虑,如果对象已经实例化,直接返回,这样做看似解决了线程安全问题,其实不然。设现有线程a和b,在t1时刻线程a和b均已通过判空语句;t2时刻时,a先取得锁资源进入临界区(被锁的代码块),执行new操作实例instance对象,然后退出临界区。t3时刻,b进入临界区,执行new操作创建实例对象。很明显Singleton被实例化两次。仍不能保证线程安全。基于此,又提出了双重锁校验式。
public class Singleton {private static Singleton instance =null;
private static Object lock =new Object();
private Singleton() {
System.out.print("private construct func...");
}
public static Singleton getInstance() {
if (instance ==null) { //1
synchronized (lock) {
if (instance ==null) {
instance =new Singleton(); //2
}
}
}
return instance;
}
}
在临界区内部再进行一次断空,解决了在临界区同时实例化对象的情况,看似解决了问题,但是仔细思考,双重锁还是存在问题。比如有线程a和b,a首先进入临界区在2处对instance进行初始化,同时b线程在外层1处判空,此时可能出现b线程返回一个只被ax线程部分初始化的instance对象,导致异常。于是提出了如下修改办法:
public class Singleton {private static Singleton instance =null;
private static Object lock =new Object();
private Singleton() {
System.out.print("private construct func...");
}
public static Singleton getInstance() {
if (instance ==null) {
synchronized (lock) {
Singleton tmpInstance =instance; // 使用临时变量
if (tmpInstance == null) {
tmpInstance =new Singleton();
}
instance = tmpInstance;
}
}
return instance;
}
}
或者:
public class Singleton {private static volatile Singleton instance =null; //注意关键字
private static Object lock =new Object();
private Singleton() {
System.out.print("private construct func...");
}
public static Singleton getInstance() {
if (instance ==null) {
synchronized (lock) {
if (instance ==null) {
instance =new Singleton();
}
}
}
return instance;
}
}
以上两种办法能很好的解决双重锁失效的问题,当然很多时候直接使用饿汉模式就能保证线程安全,而不必使用懒汉模式。