一、单例模式(Singleton Pattern)
单例模式只涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。并且提供了访问其唯一对象的方式,可以直接访问,不需要在外部实例化该类的对象。
1、单例模式可总结为以下三点:
1)构造方法使用private私有化(保证外部无法new出实例对象);
2)内部自己创建自己的唯一实例对象;
3)对外部提供方法直接访问唯一实例对象(使用static保证外部可通过类名访问)。
2、代码实现:
单例模式实现方式有两种:饿汉式和懒汉式。
1)饿汉式:不管SingleTon类对象是否使用,只要类加载就产生一个实例对象。
class SingleTon{
//在类的内部自己创建自己的实例对象
private final static SingleTon INSTANCE = new SingleTon();
//构造方法私有化
private SingleTon(){}
//对外部提供访问唯一对象的方法
public static SingleTon getInstance(){
return INSTANCE;
}
}
2)懒汉式:当第一次使用SingleTon类对象的时候,才执行实例化操作。
/*非完美版本*/
class SingleTon{
//先声明instance而不实例化
private static SingleTon instance;
private SingleTon(){}
public static SingleTon getInstance(){
//调用的时候再实例化
if(instance == null){
instance = new SingleTon();
}
return instance;
}
}
二、单例模式存在的问题:
1、懒汉式单例模式多线程下不安全
为什么说不安全呢?举个简单的例子,假设有两个线程都需要使用这个对象,线程 A 先执行语句 if (instance == null) 得到结果为 true,但还没有来得及执行语句instance = new SingleTon();此时 CPU 切换去执行线程 B,这时候由于线程A并没有执行new操作,所以线程 B 在执行语句 if (instance == null) 时又得到结果为true,紧接着线程B创建了该类的实例对象,当 CPU 重新回到线程A去执行的时候,又创建了一个类的实例对象,也就是说创建的对象不是唯一的,这就是懒汉式多线程不安全的表现。
如何解决这个问题?
方法一:使用线程安全关键字synchronized
/*使用synchronized,保证线程在创建对象的时候让其他线程阻塞*/
//写法一
public static synchronized SingleTon getInstance(){
if(instance == null){
instance = new SingleTon();
}
return instance;
}
//另外一种写法,本质上没有区别
public static SingleTon getInstance(){
synchronized(SingleTon.class){
if(instance == null){
instance = new SingleTon();
}
}
return instance;
}
使用上面这种方式,虽然可以保证多线程安全,创建出唯一的实例对象,但是在线程切换并不频繁的情况下,会对执行效率产生较大影响。
方法二:双重判空操作实现单例模式(重要)
多线程不安全是由于第一次创建对象时,恰好发生了线程切换,这种情况在后续的调用中不会再次出现,因此可在synchronized语句前增加一次判空操作,以解决方法一中存在的不足。
public static SingleTon getInstance(){
//增加一次判空后,synchronized语句只在第一次调用时执行
if(instance == null){
synchronized(SingleTon.class){
if(instance == null){
instance = new SingleTon();
}
}
}
return instance;
}
2、指令重排序
什么是指令重排序?JVM为了提高程序运行效率,在不影响单线程程序执行结果的前提下,对指令进行优化。 也就是说这种优化,在多线程下,就可能出现问题。以上面所写双重判空操作实现的单例模式为例:
问题就出在instance = new SingleTon(); 这句话的执行过程可以分为三步:
1)分配内存空间。
2)通过构造方法初始化。
3)Instance引用该内存空间。
如果发生指令重排,那么执行顺序可能发生变化:
1)分配内存空间。
2)Instance引用该内存空间。
3)通过构造方法初始化。
虽然2和3的执行顺序发生了变化,但对单线程执行结果没有影响,而在多线程情况下就会出现问题:可能会返回一个空的instance,而我们却认为是正常执行的。
解决方法:通过 volatile 避免指令重排序
volatile 关键字的作用: 1)可以保证变量的可见性; 2)阻止发生指令重排序。
/*完美版本*/
class SingleTon{
//使用volatile保证创建对象过程中不会发生指令重排
private static volatile SingleTon instance;
private SingleTon(){}
public static SingleTon getInstance(){
if(instance == null){
synchronized(SingleTon.class){
if(instance == null){
instance = new SingleTon();
}
}
}
return instance;
}
三、单例模式的其他实现方法
1、使用静态内部类实现单例模式
我们知道,静态内部类不依靠外部类的存在而存在,因此我们可以这样设计:
1)在这个静态内部类初始化的时候,生成外部类的对象。
2)然后在调用 getterInstance 方法时返回该外部类对象。
代码实现如下:
class SingleTon {
//静态内部类实现instance初始化
private static class InnerSingleTon{
private static SingleTon instance = new SingleTon();
}
//私有构造方法
private SingleTon(){
}
//提供getter方法访问instance
public static SingleTon getterInstance(){
return InnerSingleTon.instance;
}
//测试
public void print(){
System.out.println("success");
}
}
public class Test{
public static void main(String[] args) {
SingleTon .getterInstance().print();
}
}
2、枚举(enum)实现单例模式
使用枚举实现单例的方法很简单,而且 Enum 类的创建本身线程就是安全的,在这这一点上和静态内部类很相似。
public class SingleTonTest{
public enum EnumSingleTon{
INSTANCE
}
public static void main(String[] args) {
EnumSingleTon instance = EnumSingleTon.INSTANCE;
EnumSingleTon instance1 = EnumSingleTon.INSTANCE;
System.out.println(instance == instance1);
//得到true,因此实际是同一个实例对象
}
}