在标准的23个设计模式中,单例设计模式在应用中是比较常见的。但在常规的该模式教学资料介绍中,多数并没有结合多线程技术作为参考,这就造成在使用多线程技术的单例模式时会出现一些意想不到的情况,这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果。
1、立即加载/“饿汉模式”
什么是立即加载?立即加载也称为“饿汉模式”,就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化。
立即加载/“饿汉模式”是在调用方法前,实例已经被创建了,来看一下实现代码。
/** * 此代码为立即加载,缺点是不能有其他实例变量 * 因为该getInstance()方法没有同步,可能会出现非线程安全剖一 */ public class MyObject { private static MyObject myObject = new MyObject(); private MyObject(){} public static MyObject getInstance() { return myObject; } }
public class MyThread extends Thread { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }
public class Run { public static void main(String[] args) throws ParseException { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); } }
1032010069
1032010069 1032010069 |
控制台打印的hashCode是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。
2、延迟加载/“懒汉模式”
什么是延迟加载?延迟加载也称为“懒汉模式”,就是在调用get()方法时实例才被创建,常见的实现办法就是在get()方法中进行new实例化。
延迟加载/“懒汉模式”是在调用方法时实例才被创建。虽然在一个线程中只有取一个实例,但如果在多线程的环境中,就会取出多个实例。
public class MyObject { private static MyObject myObject; private MyObject(){} public static MyObject getInstance() { if (myObject == null) { //模拟在创建对象之前做一些准备性的工作 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } myObject = new MyObject(); } return myObject; } }
自定义线程和Run类同上,结果
658705244
580835623 1791140146 |
控制台打印出了3种hashCode,说明创建出了3个对象,并不是单例的,这就是“错误的单例模式”。如何解决呢?先看一下解决方案。
2.1.延迟加载/“懒汉模式”的解决方案
(1)声明synchronized关键字
既然多个线程可以同时进人getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可。
public class MyObject { private static MyObject myObject; private MyObject(){} /** * 此种synchronized锁方法的运行效率非常低下,虽然是同步运行的,但每次调用getInstance(),都要对对象上锁 * 而且下一个线程想要取得对象,则必须等上一个线程释放锁之后,才可以继续执行. */ public synchronized static MyObject getInstance() { if (myObject == null) { //模拟在创建2对象之前做一些准备性的工作 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } myObject = new MyObject(); } return myObject; } }
自定义线程和Run类同上,结果
1791140146
1791140146 1791140146 |
2.2.尝试同步代码块
同步方法是对方法的整体进行持锁,这对运行效率不是不利的。改成同步代码块能解决吗?先看一个效率不太好的例子
public class MyObject { /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */ private static MyObject myObject = null; private MyObject(){} public static MyObject getInstance() { try { /*此种写法等同于: * public synchronized static MyObject getInstance() 的写法 * 效率很低 * */ synchronized (MyObject.class) { if (myObject == null) { //模拟在创建对象之前做一些准备性的工作 Thread.sleep(3000); myObject = new MyObject(); } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } }
自定义线程和Run类同上,结果
658705244
658705244 658705244 |
同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅度提升。
使用DCL双检测锁机制来实现,继续修改这个代码:
public class MyObject { /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */ private static MyObject myObject = null; private MyObject(){} //使用双检测机制来解决问题,既保证了不需要同步代码的异步执行性,又保证了单例的效果 public static MyObject getInstance() { try { if (myObject == null) { //模拟在创建对象之前做一些准备性的工作 Thread.sleep(3000); //针对某些重要的代码进行单独的同步!!! synchronized (MyObject.class) { //如果没有这个判断,在多线程的情况下无法到得同一个实例对象!!! if (myObject == null) { myObject = new MyObject(); } } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } }
1791140146
1791140146 1791140146 |
使用双重检查锁功能,成功地解决了“懒汉模式”遇到多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。