单例模式
单例模式,顾名思义,在程序运行过程中,只允许某个对象存在一个实例,使用单例模式,你可以
- 确保一个类只有一个实例
- 提供了一个对象的全局访问指针
- 在不影响单例类的客户端的情况下允许将来有多个实例
传统的单例模式有三种,懒汉式、饿汉式和 登记式。
懒汉式
懒汉式的特点是延迟加载,在需要用到实例的时候去加载。我们书写一个最简单的懒汉式展开谈论,代码如下:
`
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对象;
但是以上依然存在问题,让我回顾一下创建对象过程
- 创建变量
- 开辟内存地址
- 将内存地址赋值给变量
但是因为重排序的问题,可能会出现
- 创建变量
- 将内存地址赋值给变量
- 开辟内存地址
当第一个线程经过两次判断开始创建对象时,假如此时发生重排序,创建一个变量,将地址赋值给这个变量,这时如果有一个线程执行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);
}
`
总结
所以有人总结,如果说懒汉式是“时间换空间”,那么饿汉式就是“空间换时间”,因为一开始就创建了实例,所以每次用到的之后直接返回就好了。