1、概念
单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
单例模式三大要点:
- 一个类只能有一个对象实例
- 必须由自身创建这个实例
- 必须对整个系统提供这个实例,需要有对外提供实例的方法
单例模式分为饿汉式和懒汉式:
- 饿汉式:在程序启动或类被加载的时候,同时创建好对象实例。
- 懒汉式:类被加载时不进行创建,当实例被使用时才创建实例,此处需判断是否已经创建过,创建过则不用再次创建,返回即可。
2、代码实现
单例模式如何保证只有一个实例?要创建实例必须通过类的构造方法来创建,那么,只要保证构造方法不能被其他类调用,构造方法私有化即可。必须对整个系统提供这个实例,所以必须有对外提供实例的方法,且这个方法为static静态方法,在懒汉式中需要对实例判断,是否需要创建。
饿汉式单例模式:
/** 饿汉式单例,立即加载**/
public class Singleton1 {
//构造方法私有化
private Singleton1(){ }
//类加载时创建实例
private static Singleton1 instance = new Singleton1();
//对外提供实例
public static Singleton1 getInstance() {
return instance;
}
}
懒汉式单例模式:
/** 懒汉式单例,调用时才创建 **/
public class Singleton2 {
//创建好空实例
private static Singleton2 instance = null;
//构造方法私有化
private Singleton2(){ }
//对外提供实例
public static Singleton2 getInstance() {
if(instance == null)
instance = new Singleton2();
return instance;
}
}
测试:
可以看出,每次获取到的实例为同一个。
3、多线程下的单例模式
在上篇中提到了发生多线程问题的3个条件,1是处于多线程环境下,2是多个线程共享一个资源,3是对资源进行非原子性操作。看下上述代码是否有线程安全性问题。
饿汉式代码中没有对资源进行非原子操作,不管多少个线程同时访问都不会出现线程安全问题,线程访问获取饿汉式单例方法只是读取操作,没有其他新建操作,所有没多线程安全问题。
上述懒汉式代码中,满足了出现多线程问题的3个条件,多个线程同时访问获取单例的方法,当有两个或者两个以上线程同时判断单例是否为空,当为空时,则会有多个线程创建新的实例,出现了多个实例,所以上述代码在多线程情况下会有问题。下面用多线程环境来测试代码,看是否会出现问题:
创建多个线程来获取单例,发现有一个实例跟其他实例不一样,出现了多个实例,即出现了多线程问题。
下面来改进代码,避免多线程问题:
在上篇中介绍了synchronized的原理,synchronized用来修饰方法或者代码块,以保证线程同步,所以只要用synchronized修饰多线程调用的获取实例的方法,就可以避免多线程问题,代码如下:
public class Singleton3 {
//构造方法私有化
private Singleton3() {}
//创建空实例
private static Singleton3 instance = null;
//对外提供实例
public static synchronized Singleton3 getInstance() {
if(instance == null)
instance = new Singleton3();
return instance;
}
}
再来测试,用线程池创建20个线程多次测试:
测试发现每次获取到的实例为同一个,没有出现多线程问题。
不过,使用synchronized对整个方法同步,加锁力度大,并发性能低,所有线程都会阻塞在方法外边。只对产生多线程问题的代码块进行同步,降低加锁力度,对instance = new Singleton3();这行代码同步,同时,在同步代码块中需要再次对instance进行空判断:
public class Singleton3 {
// 构造方法私有化
private Singleton3() { }
// 创建空实例
private static Singleton3 instance = null;
// 对外提供实例
public static Singleton3 getInstance() {
if (instance == null) {
synchronized (Singleton3.class) {
if(instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
上述方式也叫双重检查,进行了两次空检查,这也是比较完美的多线程实现方式,但是,上述代码是否还有缺陷,是否能完全避免多线程问题呢,no,还是有可能会出现多线程问题,但是很难模拟。因为有指令重排序。
操作系统为了提高运行效率,会对java字节码进行重排序,就是不一定会按照锁编写代码的顺序执行,有可能会先进行new操作,再进行空判断操作。怎么避免指令重排序呢,就是使用volatile关键字,修饰instance变量,private static volatile Singleton3 instance = null,这样就可以避免指令重排序,避免出现多线程安全问题。