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

单例模式

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

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

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

懒汉式

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

`
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);
	}
`

总结

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值