单例模式
介绍
单例模式是应用最广的模式之一。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象。
定义
确保某一个类只有一个实例,而且自行实例化向整个系统提供这个实例。
使用场景
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。创建一个对象需要消耗的资源过多。就需要考虑使用单例模式
使用单例模式的关键点:
- 构造函数不对外开放,一般为private
- 通过一个静态方法或者枚举返回单例对象
- 确保单例类的对象有且只有一个
确保单例类的对象在反序列化时不会重新构建对象
- 解释:通过将单例类的构造函数私有化,使得客户端代码不能通过new的形式手动构造单例类的对象。单例类会暴露一个公有静态方法,客户端需要调用这个静态方法获取到单例类的唯一对象,在获取这个单例对象的过程中需要确保县城安全。即在多线程的环境下构造单例类的对象也是只有一个
实例:
模拟一个公司的员工情况,一个公司有多个员工Staff 有多个副总VP 但是只有一个CEO 无论在什么情况下,CEO总保持只有一个
- Staff模型
/**
* Describe:员工模型
* wx on 2016/12/29.
*/
public class Staff {
public void work() {
System.out.println("staff is do something");
}
}
- VP模型
/**
* Describe:VP模型
* wx on 2016/12/29.
*/
public class VP extends Staff {
@Override
public void work() {
System.out.println("vp manager staff");
}
}
- CEO模型
/**
* Describe:CEO模型
* wx on 2016/12/29.
*/
public class CEO extends Staff {
private static CEO ceo = new CEO();
/**
* 私有构造方法
*/
private CEO() {
}
/**
* 静态提供实例方法
*/
public static CEO getInstance() {
return ceo;
}
@Override
public void work() {
System.out.println("ceo manager staff");
}
}
- Company模型
/**
* Describe:公司模型
* wx on 2016/12/29.
*/
public class Company {
private List<Staff> allStaffs = new ArrayList<Staff>();
public void addStaff(Staff staff) {
allStaffs.add(staff);
}
public void showAllStatffs() {
for (Staff allStaff : allStaffs) {
System.out.println("obj" + allStaff.toString());
}
}
}
- Test
/**
* Describe:
* wx on 2016/12/29.
*/
public class Test {
public static void main(String[] args) {
Company cp = new Company();
// TODO: 创建CEO对象
CEO ceo1 = CEO.getInstance();
CEO ceo2 = CEO.getInstance();
// TODO: 入职
cp.addStaff(ceo1);
cp.addStaff(ceo2);
// TODO: 创建VP
Staff vp1 = new VP();
Staff vp2 = new VP();
// TODO: VP入职
cp.addStaff(vp1);
cp.addStaff(vp2);
Staff staff1 = new Staff();
Staff staff2 = new Staff();
Staff staff3 = new Staff();
// TODO: 员工入职
cp.addStaff(vp1);
cp.addStaff(vp2);
cp.addStaff(staff1);
cp.addStaff(staff2);
cp.addStaff(staff3);
// TODO: 查看所有员工
cp.showAllStatffs();
}
}
运行结果:
objcom.individual.wx.singlemode.singleton.CEO@7d4991ad
objcom.individual.wx.singlemode.singleton.CEO@7d4991ad
objcom.individual.wx.singlemode.singleton.VP@28d93b30
objcom.individual.wx.singlemode.singleton.VP@1b6d3586
objcom.individual.wx.singlemode.singleton.VP@28d93b30
objcom.individual.wx.singlemode.singleton.VP@1b6d3586
objcom.individual.wx.singlemode.singleton.Staff@4554617c
objcom.individual.wx.singlemode.singleton.Staff@74a14482
objcom.individual.wx.singlemode.singleton.Staff@1540e19d
单例的几种实现方式
懒汉式
- 懒汉式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化,而饿汉式是在声明静态对象时就已经初始化。
- 懒汉式实现:
/**
* Describe:懒汉式单例模式
* wx on 2016/12/29.
*/
public class SingleTon {
public static SingleTon instance;
private SingleTon() {
}
public static synchronized SingleTon getInstance() {
if (instance == null) {
instance = new SingleTon();
}
return instance;
}
}
- 存在问题:
懒汉式单例模式实现中,即使instance已经被初始化,调用getInstance还是会进行同步,这样会消耗不必要的资源。
Double Check Lock (DCL) 实现单例
- DCL实现单例模式的优点是既能在需要时才初始化单例,又能保证线程安全,且单例对象初始化后调用getInstance不进行同步锁
/**
* Describe:Double Check Lock 实现单例模式
* wx on 2016/12/29.
*/
public class DCLSingleTon {
private static DCLSingleTon instance = null;
private DCLSingleTon() {
}
public static DCLSingleTon getInstance() {
// TODO: 第一次判空为了避免不必要的同步
if (instance == null) {
synchronized (SingleTon.class) {
// TODO: 第二次判空为了没有实例时创建实例对象
if (instance == null) {
instance = new DCLSingleTon();
}
}
}
return instance;
}
}
explain: 可见整个程序进行了两次非空判断,第一次的非空判断是为了避免不必要的同步,也就是说在instance为空的时候,也就是需要的时候才进行同步锁定。第二次非空判断,是为了实例并不存在的时候初始化。
DCL单例模式的有缺点
- 缺点:第一次加载的时候反应会稍微慢,也会由于java内存模型的原因偶尔会失败
- 优点:资源利用率高,在第一次执行getInstance()时单例对象才会被实例化,效率高
静态内部类实现单例模式
- DLC虽然在一定程度上解决了资源消耗、多余同步、线程安全等问题,但是,在某些情况下还是会出现失效的问题。这个问题被称为双重检查锁定失效,在《Java并发编程实践》中给出优化方案:
/**
* Describe:静态内部类实现单例模式
* wx on 2016/12/29.
*/
public class InnerSingleTon {
private InnerSingleTon(){}
public static InnerSingleTon getInstance(){
return InnerSingleTonHolder.sInstance;
}
private static class InnerSingleTonHolder {
public static InnerSingleTon sInstance = new InnerSingleTon();
}
}
- 第一次加载InnerSingle类时并不会初始化sInstance,只有在第一次调用getInstance()的时候才会导致sInstance被初始化。
反序列化的问题
通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例,即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于用该类的构造函数,反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化方法readResolve(),这个方法可以让开发人员控制对象的反序列化。例如要杜绝单例对象在被反序列化时重新生成对象。那么必须加入如下方法:
private Object readResolve() throws ObjectStreamException{
return sInstance;
}
枚举单例
使用枚举单例是好处是,写法简单,默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。枚举在反序列化时也不会重新生成新的实例
/**
* Describe:枚举单例
* wx on 2016/12/29.
*/
public enum EnumSingleTon {
INSTANCE;
public void doSomeThing(){
System.out.println("enm do sth.");
}
}
使用容器实现单例
/**
* Describe:容器单例
* wx on 2016/12/29.
*/
public class SingleTonManager {
private static Map<String, Object> objMap = new HashMap<String, Object>();
private SingleTonManager() {
}
public static void regiserService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
- 在程序的开始,将许多单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式可以让我们管理多种类型的单例。并且在使用时可以通过统一的接口进行获取操作
Android源码中的单例模式
- 在Android系统,我们会经常会通过Context获取系统级别的服务,如WindowManagerService、ActivityManagerService等。更常用的是一个LayoutInflater的类。这些服务会在合适的时候以单例的形式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)获取。
- 在Android系统中,还有类似于LayoutInflate也是在运行时先注册到系统服务器管理中,然后在使用时直接根据名字来进行使用的单例模式
- 由于水平比较菜,所以还不不贴源码了,但是在Android源码中,还是有很多单例的设计模式的使用地方的,一定要去看看啊
总结
虽然在客户端中没有太多的高并发的情况,因此单例模式的使用与否并不会有太大的影响。在使用单例模式时一定要注意由于系统中只存在一个对象,那么对于这个对象的赋值和操作会影响到整个系统的使用,在这点还是应该注意的。一般对于资源消耗大的类,方便程序的性能提升 还是比较推荐使用单例模式来进行对象的管理的。
优点:
1.由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建和销毁时,单例模式就非常明显了
2. 由于单例模式只生成一个实例,所以减少了系统性能的开销。当一个对象的产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个对象,然后永久驻留内存的方式来解决
3. 单例模式可以避免对资源的多重占用。
4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问。
缺点
1.单例模式一般没有接口,拓展比较困难
2.单例对象如果持有Context,那么很容易发生内存泄漏,需要注意传递给单例对象的Context最好是Application Context.