设计模式之单例模式

单例模式是日常开发中经常使用的模式之一,需要大家对它有足够的了解。尽管单例模式理解相对其它模式简单,但是在一些特殊情况下,实现单例模式也需要特别注意。

定义

用来创建独一无二的,只有一个对象实例的类。在实际项目中,单例模式使用非常频繁,例如:线程池、数据库连接池、缓存、日志对象、Spring默认Bean对象等。在这些情况下,若使用过多实例,可能会导致系统资源使用过量、数据不一致等问题。

大家可能会问,创建全局静态变量也能做到如此效果,为什么还会出现一个单例模式呢?没错,全局静态变量确实可以实现如此效果,可是全局变量是在程序启动时就已经创建好对象了,如果该对象占用极大内存并且使用相对较少,就会造成内存资源浪费。使用单例模式,我们可以在需要的时候才去创建对象并且只实例化一次。

实现

单例模式根据实现方式可以分为两种:懒汉式和饿汉式。

懒汉式,顾名思义就是使用的时候才会进行实例化,延迟加载。代码如下:

public class Singleton {
		private static Singleton INSTANCE;
	
		private Singleton(){
			if (INSTANCE != null) {
				throw new RuntimeException(this.getClass() + "不具有该Constructor方法");	
			}
		}
		public static Singleton newInstance() {
                  if(INSTANCE==null){
                       INSTANCE = new Singleton();
                   }
			return INSTANCE;
		}
}

使用private修饰构造方法,为了防止该类被实例化。另外构造函数中判断INSTANCE是否为空,是为了防止使用反射强行执行构造函数实例化,违反单例结果。

饿汉式,顾名思义就是一开始就进行实例化。代码如下:

   public class Singleton {
		private static final Singleton INSTANCE = new Singleton();
		private Singleton() {
			if (INSTANCE != null) {
				throw new RuntimeException(this.getClass() + "不具有该Constructor方法");	
			}
		}
		public static Singleton newInstance() {
			return INSTANCE;
		}
    }

大家是不是觉得单例模式特别简单,其实上述两种实现方式还是有点问题。以懒汉式来说,newInstance()方法,其中我们判断INSTANCE是否为空,不为空则进行实例化,这段代码在单线程中运行确实没有什么问题,但是在多线程环境中,假设现在有两个线程同时判空,那么两个线程都会进行实例化,这个时候,就是有问题的。下面我们就来简单介绍一下,多线程环境中如何实现单例模式。

线程安全的单例

饿汉式是在JVM加载该类的时候就会进行实例化,所以它是线程安全的。上述实现的懒汉式代码,如何解决才能实现线程安全的呢?大家首先想到的应该是加锁,没错,最简单的实现方式是这样的:

synchronized

public static synchronized Singleton newInstance() {
        if(INSTANCE==null){
            INSTANCE = new Singleton();
         }
         return INSTANCE;
}

采用synchronized同步关键字,能够确保多线程执行的时候,只有一个线程能够真正的进行实例化。这种方式简单是简单,可是有不好的地方,当我们第一次调用的时候,确实需要同步,但是在以后调用的时候,多个线程每次只能有一个线程获得实例,这性能肯定是不好的。

双重检查锁

上述我们知道,单纯的加锁会引起性能问题。现在我们思考一下,有没有一种方式,只会在第一次的时候进行加锁,以后都不会涉及同步。双重检查锁就是这样,先判断对象是否为空,再进行加锁进行实例化,这样只会在第一次的时候是同步操作,以后都不会经过锁操作,避免出现性能问题。代码如下:

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

大家请注意,使用双重检查锁的时候,一定要在申明变量的时候加上volatile关键字,例如:

private volatile static Singleton singleton;

这是为什么呢?在INSTANCE = new Singleton();这一行代码中,分为三个步骤:1.分配内存空间。2.初始化对象。3.将对象指向分配的内存空间。关于这三个步骤,由于2和3两个步骤之间没有太大关联,所以编译器在执行的时候,可能会进行指令重排,导致2和3执行顺序颠倒,引起另外一个线程读取到还未进行初始化的对象。为了解决这个问题,就必须使用volatile,它会防止指令重排。

上述都是采用加锁的方式实现线程安全,有没有不使用加锁的方式呢?肯定有,比如下面我们使用的这两种方式,

内部类实现

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

这种方式不仅是线程安全的,并且是延迟加载的,只有在第一次调用newInstance()时,才会创建Singleton实例。这里主要应用了内部类和类的初始化方式,内部类被申明为private,外部类不能对它进行初始化,只能由newInstance()方法进行初始化。这种方式没有涉及到锁操作,所以在高并发的情况下性能优越。

枚举实现

枚举的特性应该不用介绍吧,天生就是单例的,所以使用枚举方式,性能也是非常优越的。

public enum Singleton {
		INSTANCE();
		public void print(){
			System.out.println("我只是一个单实例:"+this);
		}
}

总结

单例模式分为懒汉式和饿汉式。对于频繁使用的对象,特别是一些占内存较大的对象,使用单例模式可以节约资源,并且由于new操作的次数减少,可以减轻GC的压力。在多线程的环境下,要注意单例模式的线程安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值