单例模式详解(一)
单例模式应该说是最简单的一种设计模式,单例模式应该确保单例类在系统中绝对只有一个实例被创建,并且提供一个全局的访问点,属于创建型模式。
生活中的单例场景也非常多,比如古代一个国家只有一个皇帝,一个公司只有一个CEO等等。我们熟知的spring框架中的ApplicationContext、数据库中的连接池等也都是单例模式的实现。对于Java来说,单例模式也用于保证在一个JVM中只存在单一的实例。
单例模式适用于以下场景:
- 对于某些经常使用且需要频繁创建对象的类,使用单例模式可以节省内存空间,减少无谓的GC;
- 频繁访问数据库或文件的对象;
- 对于使用多个实例对象可能产生error的类,比如一些从系统上来讲是单一控制逻辑的操作,如果有多个实例进行操作,系统会出现乱套。
如此看来,单例模式其实适用的场景还是挺多的,接下来我们看看最简单也是最原始的一种单例实现方式:
饿汉式单例模式:
/** * 饿汉单例模式 */ public class Singleton { //直接声明一个静态变量并进行实例化 private static final Singleton instance = new Singleton(); private Singleton(){ } public static Singleton getInstance(){ //返回已经初始化的实例 return instance; } }
没错,上面的实现方式是一种饿汉式单例的标准写法。需要注意的是,一般单例模式的构造方法会进行私有化(防止new的操作创建多余的对象),其实是提供一个全局访问的方法,饿汉式单例的实现有以下几个特点:
1、饿汉式单例写法的单例用static关键字声明变量后直接new创建单例对象,这样在类加载的时候就进行对象初始化。
2、由于在类加载的时候就进行对象的初始化类,所以它绝对是线程安全的,因为在线程还没出现的之前就已经实例化了,所以是不可能存在线程安全问题。
当然这种写法也有缺点,众所周知,计算机的内存资源是极其珍贵的,而这种写法由于在类加载之初就进行实例化了,当系统中有大量这种饿汉式写法的单例出现时,在系统初始化的时候就会造成内存的浪费,从而导致系统内存不可控。不管单例对象在系统运行中,是否使用,都会占用内存空间。那有没有更优的写法呢?当然有,于是乎,出现了我们的懒汉式单例模式。
懒汉式单例(一):
public class Singleton { //直接声明一个静态变量 private static Singleton instance = null; private Singleton(){} public static Singleton getInstance(){ if(instance==null){ //如果实例为null才进行初始化 instance = new Singleton(); } return instance; } }
这样写看似就没有问题了,在调用getInstance()方法的时候,会进行一次判断,只有当单例对象为null的时候才进行实例化的创建。然而事实告诉你,这样写肯定是有问题的,并且问题是很大的!在如今多核心cpu的时代下,多线程产生的线程安全问题不能不考虑!如果由于多线程共同访问getInstance方法而导致系统中出现了多余的单例对象,这不仅违背了单例模式的初衷,还会产生error,这是我们必须要引起重视的!我们运用多线程场景测试一下线程不安全的场景:
public class ExcutorThread implements Runnable { @Override public void run() { Singleton singleton = Singleton.getInstance(); System.out.println(Thread.currentThread().getName()+":"+singleton); } }
测试一下:
public static void main(String[] args) { //创建两个线程 Thread t1 = new Thread(new ExcutorThread(),"thead-1"); Thread t2 = new Thread(new ExcutorThread(),"thead-2"); t1.start(); t2.start(); }
测试结果:

果然,在测试中出现了两个不通的实例。出现这样的结果也是有一定的概率的,当Thread-1和Thread-2同时访问到 if(instance==null)时,此时对象并没有初始化,两个线程同时进行了对象的实例化,创建了两个单例对象。既然要考虑线程安全隐患,这种单例的写法自然也是不可取的,那我们让其线程安全不就可以了吗?于是乎就有了下面这种单例写法:
懒汉式单例(二):
public class LazySyncSingleton { private static LazySyncSingleton singleton = null; private LazySyncSingleton(){ } public static synchronized LazySyncSingleton getInstance(){ if(singleton==null){ singleton = new LazySyncSingleton(); } return singleton; } }
就这样,我们的懒汉式单例二正式登场,最主要的是它还是线程安全的。但是又总觉得哪里不太好,这样锁的粒度是不是太大了,会不会影响性能?如果能考虑到性能问题,恭喜你,又进步了!
的确,这样写仍然存在问题!当线程并发竞争很激烈的时候,大家都要获取单例对象,而此时又全部需要排队获取,会导致大量的线程阻塞,从而导致程序的性能大幅下降,你可以想一想大家都在排队买茶颜悦色的奶茶的场景吧~就知道有多不可取了!终于也迎来了我们的双重检查锁的单例写法:
懒汉式单例(三):
public class LazyDoubleCheckSingleton { private static volatile LazyDoubleCheckSingleton singleton = null; private LazyDoubleCheckSingleton(){} public static LazyDoubleCheckSingleton getInstance(){ //检查是否需要加锁 if(singleton==null){ synchronized (LazyDoubleCheckSingleton.class){ if(singleton==null){ //检查对象是否需要创建 singleton = new LazyDoubleCheckSingleton(); } } } return singleton; } }
仔细回顾一下代码逻辑,当第一个线程调用getInstance()方法时,第二个线程也可以调用。当第一个线程执行到synchronized时会上锁,第二个线程执行到synchronized时就会被阻塞,当第一个线程执行完对单例对象对实例化后,第二个线程会先检查对象是否已经创建,由于已经被第一个线程创建了,会直接返回单例对象。看来双重检查锁的单例写法能解决线程安全问题了,性能似乎也不错,仅仅只有在初次调用getInstance()对实例初始化对时候会发生阻塞,其余对情况基本不会有多的性能损害,这样的写法当然也是可以用到生产环境中的,但是追求完美的哥们会觉得,用了synchronized关键字总归是要上锁,还是有使线程阻塞了,能不能不使用synchronized就保证线程安全而又不浪费内存呢?
当然有,欲知后事如何,请听下回分晓。

浙公网安备 33010602011771号