`

Java深入学习之单例模式

阅读更多
Java设计模式自学之单例模式

对于单例模式来说,最重要的就是私有构造函数,提供静态的实例化方法,所以单例模式的几个关键字:private 的构造函数,public static 提供的实例化方法,private static 的实体类成员变量,只要满足这三个基本的要素,就能实现单例模式。

1、懒汉模式

懒汉模式是最基本的单例模式之一,满足最基本的单例模式条件

  
 public class Single1 {
	    
		private static Single1 single = null;
		//私有构造函数
		private Single1(){
			
		}
		//提供实例化方法
		public static Single1 getInstance(){
			if (null == single) {
				single = new Single1();
			}
			return single;
		}
 }

以上代码就实现的懒汉单例模式,在初始化时不会创建对象实例,只要当getInstance被调用时才会创建。懒汉嘛,就是不到最后一刻不去真正做事,就是形容开始时不创建,要用的时候才创建实例。这种方式降低了初始时的内存空间,但后续的调用都需要判断的时间。

上面的懒汉模式在多线程下会出现安全问题,线程安全就是多个线程同时访问会出现安全性问题。比如一个房间,只有第一个进入的人才能留下自己的签名,其他人发现房间里有签名时,就不能再签名了。但如果两个人同时进门,两者发现房间里面没签名,都可以进入房间留下签名,就不符合房间只能存在一个签名的场景,所以这种情况下就需要给房间加锁。

懒汉模式变种一

这种情况是直接给房间门上把锁,第一个进入的人锁住门,后面的人就无法在他签名时再次进入房间。不算有多少人同时到达,但是只有一个人能拿到锁,所以不会存在线程安全问题。

public class Single2 {
	
	private static Single2 single = null;
		
	private Single2(){
			
	}
		
	public static Single2 getInstance(){
	       //使用锁关键字
		synchronized(Single2.class){
			if (null == single) {
				single = new Single2();
			}
		}
		return single;
	}
}


这种加锁方式与在getInstance方法上加锁效果是一样的。但是这种方式有很大的效率问题,就是外面的人,都需要等里面的人把门打开,才能发现房间里是不是有签名了,因为门被锁住时,大家不知道里面的人会不会真的留下签名,如果人一旦过多,就会等待(Java跟人不同,不会因为房间被锁住就退走,而是继续等待),其实大家只需要看下里面是否有签名,而看签名的时间比起锁门再解锁的时间根本不是一个层级上的,这样就造成效率过低。

懒汉模式变种二

另外一种方式就是给房间加上一扇窗户。大家进入房间时,先通过窗户看下,里面有没有签名,有的话直接退走,没有的话就继续往门方向走。由于窗户是大家都可以看的,需要等待的只有在通过窗户发现里面没签名的人才会继续往前走,比起上一种人人都需要等待时间消耗少非常多。

public class Single3 {
	
	private static Single3 single = null;
		
	private Single3(){
			
	}
		
	public static Single3 getInstance(){
           //先判断是否为空,相当于一扇窗户
		if (null == single) { 
			synchronized(Single3.class){
				if (null == single) {
					single = new Single3();
				}
			}
		}
		return single;
	}
}


这种方式也叫做双重锁校验机制。理论上这种方式是完全没问题的,但是由于Java虚拟机加载机制的问题,在JDK1.5之后,需要给成员变量添加一个volatile关键字,它可以保证变量的可见性和有序性,被它修饰的变量的值,不会被本地线程存储,所有对其修饰变量的操作都是直接共享内存,保证多个线程可以同时可见。

即:private volatile static Single3 single = null,这样才能完整的保证这种方式是在任何情况下都是具备正确性的。

补充:Java虚拟机加载过程主要可以分为三个步骤:装载、连接和初始化

1、装载阶段:就是将java文件对应的.class文件以二进制数据加载到JVM中,加载的实例和类位于堆中,然后创建一个java.lang.Class对象来封装类信息数据结构,而类信息则被放到方法区中。以“类的全限定名+ClassLoader实例ID”来标明这些类,这里也会涉及到“双亲委派模型”机制。

2、连接阶段:这个阶段分为三个步骤,
  • 步骤一:验证,验证这个class文件里面的二进制数据是否符合java规范,并且符合当前JVM;
  • 步骤二:准备,为该类的静态变量分配内存空间并赋值为默认值;
  • 步骤三:解析,将类的常量池中的符号引用解析为直接引用,也可以在用到相应的引用时再解析。


3、初始化:初始化类中的静态变量,并执行类中的static代码、构造函数。初始化顺序:
  • 1. 为静态变量分配内存并赋值或者执行静态代码块;
  • 2. 为非静态属性分配内存并赋值;
  • 3. 构造方法;
  • 4. 执行非静态代码块 或 静态方法(都是调用了才加载)。

在JVM中存在一个很大的问题就是加载过程并不是时时都是有序的,内存模型中允许存在“无序写入”。比如:single = new Single3();这段代码就不是原子性操作,在JVM处理时大概可以分为三步。
* 第一步,给Single3分配内存;
* 第二步,初始化Single3的构造器;
* 第三步,将single对象执行已经分配内存空间Single3**(此时,single已经不是null,而是有空间的内存)**;

但是由于该语句并不是原子操作,所以这三步执行在JVM实际的顺序可能是1,3,2这样执行,所以,如果线程B执行到第一个if(null == single)时而线程A恰好是在1,3,2中的3时,线程B拿到的也不是一个非null的对象,而是一个没有值得内存空间,导致直接返回,但是实际上是没有数据的,从而造成了线程安全的问题。

2、饿汉模式
当然,如果在懒汉模式中初始化成员变量是直接就进行赋值,是什么情况呢

public class Single4 {
	
	private static Single4 single = new Single4();
		
	private Single4(){
		
	}
	public static Single4 getInstance(){
		return single;
	}
}


在类初始化的时候,按照JVM初始化方式,Single4 single = new Single4() 在类初始时就会被实例化,但是可能等到程序结束也不会被调用,所以这种方法称为“饿汉模式”。这种方式根据JVM本身的特性,不会存在线程安全问题,但是在初始化时就会占据内存空间。

3、内部类方式
将饿汉模式和懒汉模式总体结合归纳下,两种都有一定的利弊性,而另外一种方式完美的融合两种方式的问题。

public class Single5 {

	/**
	 * 加载类时,内部类实例化与外部类没有绑定关系,
	 * 所以只有在调用时才会加载,实现延迟加载
	 * 而且内部类初始化时就实例化了变量,并且只有一次,保证了线程安全
	 */
	private static class Instance{
		private static Single5 single = new Single5();
	}
		
	public static Single5 getInstance(){
		return Instance.single;
	}

}


这种方式即实现了延迟加载,也保证了线程安全,所以是用得最多的一种方式。

4、单例方法屏蔽
在使用单例模式中,可以通过其他途径创建其他实例:
第一种;通过反射构造单例对象,反射时可以使用setAccessible方法来突破private的限制,获取到新的实例,而打破单例模式;

public static Single5 refCopy() throws Exception{
    	//通过反射获取构造函数
	Constructor<Single5> con = Single5.class.getDeclaredConstructor();
	con.setAccessible(true);
	//获取实例
	Single5 refTest = con.newInstance();
	return refTest;
}

   

第二种;通过反序列化构造单例对象。

/** 
    * 序列化克隆 
    * @return 
    * @throws Exception 
    */  
   public static Single5 deepCopy() throws Exception{  
       ByteArrayOutputStream os = new ByteArrayOutputStream();  
       ObjectOutputStream oos = new ObjectOutputStream(os);  
       oos.writeObject(getInstance());  
         
       InputStream is = new ByteArrayInputStream(os.toByteArray());  
       ObjectInputStream ois = new ObjectInputStream(is);  
       Single5 test = (Single5) ois.readObject();  
       return test;  
}


测试代码:
  
Single5 test = Single5.getInstance();
Single5 test1 = Single5.getInstance();
	
Single5 test3 = Single5.deepCopy();
	
Single5 refTest = Single5.refCopy();
	
System.out.println(refTest.equals(test));
System.out.println(test == test1);
System.out.println(test3.equals(test1));


测试结果分别为false,true,false;由此可见,以上两种方法都能破坏单例模式。所以,要完完全全的实现单例模式,必须需要进行一些完善,比如序列化时,添加readResolve方法,返回获取的instance对象;而反射,则需要跟多的权限处理等。
 public Object readResolve(){
	return getInstance();
}

分享到:
评论

相关推荐

    java单例模式实例

    单例模式是软件设计模式中的一种经典模式,用于确保一个类只有一个实例,并提供一个全局访问点。...通过学习和实践这些单例模式的实现,我们可以更好地理解和应用设计模式,提升代码的质量和可维护性。

    设计模式之单例模式(结合工厂模式)

    在项目中,`src`目录可能包含了这些设计模式的源码示例,可以用来学习和理解如何实际应用单例模式和工厂模式。通过阅读和分析这些代码,你可以更深入地理解这两种模式的实现细节及其在实际开发中的作用。同时,也...

    设计模式单例模式和工厂模式综合应用

    总的来说,这个项目为学习和理解单例模式和工厂模式在实际开发中的应用提供了一个很好的示例。通过结合这两种模式,可以构建出既保证了对象唯一性,又能灵活应对不同产品创建需求的系统。对于Java开发者来说,深入...

    java 设计模式 mvc模式 单例模式 代理 工厂 简单工厂 第二部分

    本篇将深入探讨标题中提及的几种设计模式:Model-View-Controller(MVC)模式、单例模式、代理模式以及工厂模式,尤其是简单工厂模式。 **1. Model-View-Controller (MVC) 模式** MVC模式是一种架构模式,它将应用...

    JAVA单例模式的登录案例

    根据给定的信息,我们可以深入探讨Java单例模式的登录案例,并从中提炼出多个重要的知识点。 ### 单例模式概述 单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。这种模式通常...

    设计模式 中的 单例模式和观察者模式

    首先,让我们深入了解单例模式。单例模式是一种确保一个类只有一个实例,并提供全局访问点的设计模式。这种模式在资源管理、缓存、对话框、注册表设置、日志记录等场景中非常有用。为了实现单例,通常我们会创建一个...

    研磨设计模式之单例模式

    通过研磨设计模式之单例模式的资料,你可以深入理解单例模式的原理、实现方式及其优缺点,进一步提升自己的编程技能和设计思维。学习并熟练掌握设计模式,对于成为一名优秀的Java开发者至关重要。

    Java聊天室 观察者模式和单例模式

    Java聊天室是一个典型的面向对象设计的应用,它利用了观察者模式和单例模式来实现高效、灵活的通信机制。在本文中,我们将深入探讨这两个设计模式以及它们在Java中的应用。 首先,观察者模式(Observer Pattern)是...

    最简单的单例模式源码

    接下来,我们将深入探讨单例模式的实现原理,以及如何通过提供的`Singleton.java`文件实现一个简单的单例。 首先,单例模式的基本要求是: 1. **唯一性**:确保类只有一个实例。 2. **全局访问**:提供一个全局...

    java单例设计模式

    通过这个小练习,初学者可以了解到单例模式的核心思想和实现方式,同时也可以深入理解Java中的类加载机制、线程同步以及volatile关键字的作用。通过实践这些代码,可以提高对Java内存模型和多线程的理解,进一步提升...

    java 设计模式之单例的实例详解

    Java中的单例模式有多种实现方式,其中最常见的是“饿汉式”和“懒汉式”。 1. **饿汉式**: 饿汉式是在类加载时就完成了初始化,因此也被称为静态初始化。在Java代码中,饿汉式通常表现为将单例对象在类加载时...

    java 单例模式

    在软件设计模式中,单例模式是一种常用的模式,它的核心思想是确保一个类只有一个实例,并提供一个...通过阅读源码和注释,可以更深入地理解单例模式的原理和应用。同时,欢迎其他大牛提出宝贵意见,共同探讨和学习。

    单例模式(Singleton)的6种实现

    在学习和应用单例模式时,还需注意单例模式的扩展性和测试性问题。例如,单例类不应该被继承,否则每个子类都可以创建实例,这违背了单例模式的初衷。另外,在测试时,单例类往往难以进行单元测试,因为它们没有公共...

    菜鸟学习java设计模式之单例模式

    在Java中,单例模式的实现有很多种方式,包括懒汉式、饿汉式、双重检查锁定(DCL)等。下面我们将深入探讨这些实现方式以及它们的优缺点。 1. 饿汉式: 饿汉式单例在类加载时就完成了实例化,确保了线程安全。这种...

    J2EE +单例模式 中文文档

    学习J2EE和单例模式,不仅可以深入理解Java企业级开发的原理,还能提升代码质量和系统性能。这份"J2EE +单例模式 中文文档"应该包含了这两个主题的相关知识,包括理论介绍、代码示例和实践应用等,对于Java初学者来...

    Android源码学习之单例模式应用及优点介绍

    接下来,我们将深入探讨单例模式的实现原理、优点以及在Android源码中的应用。 1. **单例模式实现** - **懒汉式(懒初始化)**:只在第一次调用getInstance()方法时进行实例化,这是为了延迟加载,提高程序启动...

    java深入学习就靠他了

    "Java深入学习就靠他了"这个资源显然旨在帮助有经验的Java开发者深化对这门语言的理解,尤其是那些正处于技术突破阶段的程序员。它涵盖了Java的核心技术和高级特性,旨在提升开发者在J2SE(Java标准版)和J2EE(Java...

    深入浅出java设计模式(高清中文PDF)

    在Java中,单例模式通常通过私有构造函数和静态工厂方法实现。它广泛应用于系统配置、线程池、缓存管理等场景,确保了资源的统一管理和高效利用。 其次,"工厂模式"是创建型设计模式的一种,它提供了一种创建对象的...

    【IT十八掌徐培成】Java基础第19天_05_单例模式.zip

    这个教程“【IT十八掌徐培成】Java基础第19天_05_单例模式”很可能深入讲解了如何在Java中实现单例,以及单例模式在实际应用中的重要性和原理。 单例模式的核心思想是确保一个类只有一个实例,并提供一个全局访问点...

Global site tag (gtag.js) - Google Analytics