Java设计模式——死磕单例模式

来源:互联网 发布:matlab求逆矩阵 编辑:程序博客网 时间:2024/05/17 08:24

单例模式

单例模式,顾名思义,在程序运行过程中,只允许某个对象存在一个实例,使用单例模式,你可以

  • 确保一个类只有一个实例
  • 提供了一个对象的全局访问指针
  • 在不影响单例类的客户端的情况下允许将来有多个实例

传统的单例模式有三种,懒汉式、饿汉式和 登记式。

懒汉式

懒汉式的特点是延迟加载,在需要用到实例的时候去加载。我们书写一个最简单的懒汉式展开谈论,代码如下:

`public class Tom {    private static Tom tom=null;    private Tom(){//私有化构造,保证类外不可以创建实例    }    public static Tom getInstance(){//返回实例        if(tom==null){             tom = new Tom();        }        return tom;    }}`

这段代码理解虽然简单,但有一处要提一下这种懒汉式其实是不安全的,当多线程并发的调用getInstance()还是有可能会创建两个实例,我们尝试给方法加上锁,修改如下:

`public class Tom {    private static Tom tom=null;    private Tom(){//私有化构造,保证类外不可以创建实例    }    //添加锁,保证线程同步        public static synchronized Tom getInstance(){        if(tom==null){             tom = new Tom();        }        return tom;    }}`

以上修改虽然保证真正只有一个实例,但是我们实现的方法是加锁,势必会影响到其他线程访问的效率,聪明的人们就又想到了双重判断,所以下面的代码也就是最常看到的单例模式
`
public class Tom {
private static Tom tom=null;
private Tom(){//私有化构造,保证类外不可以创建实例
}

    public static synchronized Tom getInstance(){        if(tom == null){                                          synchronized (Tom.class){                                if(tom == null){                                          tom = new Tom();                            }            }        }        return tom;    }}`

这段代码看起来很完美,理由如下:

  • 如果检查第一个tom不为null,则不需要执行下面的加锁动作,提高了程序的性能;
  • 如果第一个tom为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;
  • 当第一个获取锁的线程创建完成后tom对象后,其他的在第二次判断tom一定不会为null,则直接返回已经创建好的tom对象;

但是以上依然存在问题,让我回顾一下创建对象过程

  1. 创建变量
  2. 开辟内存地址
  3. 将内存地址赋值给变量

但是因为重排序的问题,可能会出现

  1. 创建变量
  2. 将内存地址赋值给变量
  3. 开辟内存地址

当第一个线程经过两次判断开始创建对象时,假如此时发生重排序,创建一个变量,将地址赋值给这个变量,这时如果有一个线程执行getInstance,在第一次判断时,tom不为null,所以直接return tom,但是此时的tom,仅仅只是一个地址,那么在使用时,调用这个对象的某个方法,却发现这个变量指向的内存,什么都没有!这时会抛什么异常呢!

问题如上,既然知道了问题源头,那么想办法解决就好了,解决办法其实很简单,只需要加上volatile修饰就行了

`public class Tom {    //通过volatile修饰    private volatile  static Tom tom=null;    private Tom(){//私有化构造,保证类外不可以创建实例    }    public static synchronized Tom getInstance(){//返回实例        if(tom == null){                                          synchronized (Tom.class){                                if(tom == null){                                          tom = new Tom();                            }            }        }        return tom;    }}`

当tom声明为volatile后,步骤2、步骤3就不会被重排序了,也就可以解决上面那问题了。
这种解决方案的实质是:允许步骤2和步骤3重排序,但是不允许其他线程看见。

饿汉式

饿汉式的特点是一开始就加载了。变量tom是使用static修饰的,只会在程序启动的时候new出一个实例,以后每次getInstance()都返回同一个tom

`public class Tom {    //设立静态变量,直接创建实例      private static Tom tom = new Tom();      private Tom(){  //私有化构造,保证类外不可以创建实例    }     public static Tom getInstance(){          return tom;      } }`

登记式

1.新建父类,定义map集合,用来存子类的实例

`public class Person {    // 设立静态变量,直接创建实例      private static Map<String, Person> map = new HashMap<String, Person>();      // -----受保护的-----构造函数,不能是私有的,但是这样子类可以直接访问构造方法了      protected Person() {      }      public static Person getInstance(String name) {          if (name == null) {              name = Person.class.getName();           }          if (map.get(name) == null) {              try {                  map.put(name, (Person)Class.forName(name).newInstance());              } catch (InstantiationException e) {                  e.printStackTrace();              } catch (IllegalAccessException e) {                  e.printStackTrace();              } catch (ClassNotFoundException e) {                  e.printStackTrace();              }          }        return map.get(name);      }  }`

2.新建子类,子类继承父类,用包名做key将实例保存在父类的map

`    public class Boy extends Person{         public static Boy getInstance() {                  return (Boy)Boy.getInstance(Boy.class.getCanonicalName());           }      }    public class Girl extends Person{         public static Girl getInstance() {                  return (Girl)Girl.getInstance(Girl.class.getCanonicalName());           }      }`

3.测试代码

`    //测试代码     public static void main(String[] args) {        Boy boy1 = Boy.getInstance();        Boy boy2 = Boy.getInstance();        System.out.println(boy1==boy2);        Girl girl1 = Girl.getInstance();        Girl girl2 = Girl.getInstance();        System.out.println(girl1==girl2);    }`

总结

所以有人总结,如果说懒汉式是“时间换空间”,那么饿汉式就是“空间换时间”,因为一开始就创建了实例,所以每次用到的之后直接返回就好了。