`

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设计优化之单例模式

    主要为大家详细介绍了java设计优化中的单例模式,深入学习java单例模式,感兴趣的朋友可以参考一下

    Java设计模式教程

    该资料介绍Java各类开发模式,包含以下教程:《深入浅出设计模式(中文版)》《Java单例模式》《Java设计模式-图解-附代码》《JAVA设计模式之单例模式(完整版)》《Java学习笔记(必看经典)》《Java总复习》《单例模式》...

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

    文件类型为PDF文件,此文档对20多种java设计模式进行了详细讲解,在中文讲解的过程中还附有代码示例给学习者进行参考,使学习者通过实践更容易理解设计模式的原理。 本文档目录: 1.工厂模式 2.单例模式 3.建造...

    【Java设计模式】你对单例模式了解多少,一文深入探究

    目录单例模式懒汉式单例模式未初始化问题解决Double Check 双重检查方案一:不让第二步和第三步重排序-DoubleCheck方案二:基于类初始化-静态内部类饿汉式饿汉式与懒汉式最大区别序列化破坏单例模式原理枚举单例基于...

    [Java设计模式(第2版)(Design.Patterns.in.Java).John.Metsker

    《java设计模式(第2版)》通过一个完整的java项目对经典著作design patterns一书介绍的23种设计模式进行了深入分析与讲解,实践性强,却又不失对模式本质的探讨。本书创造性地将这些模式分为5大类别,以充分展现各个...

    2023java最新学习路线.docx

    5. 设计模式:学习常见的设计模式,如单例模式、工厂模式、观察者模式等,了解如何应用设计模式解决常见的软件设计问题。 第七阶段:企业级项目实战 在第七阶段,您将参与一个实际的企业级项目,将之前学到的...

    深入浅出设计模式(中文版电子版)

    3.6SingletonPattern(单例模式) 82 3.6.1定义 82 3.6.2现?抵械牡ダ??猈indowsTaskManager 83 3.6.3C#实例——负载均衡控制器 84 3.6.4Java实例——系统日志 86 3.6.5DoubleCheckLocking(双检锁) 89 3.6.6...

    深入浅出设计模式(中文版)

    3.6SingletonPattern(单例模式) 82 3.6.1定义 82 3.6.2现?抵械牡ダ??猈indowsTaskManager 83 3.6.3C#实例——负载均衡控制器 84 3.6.4Java实例——系统日志 86 3.6.5DoubleCheckLocking(双检锁) 89 3.6.6...

    design-pattern-java.pdf

    工厂三兄弟之抽象工厂模式(二) 工厂三兄弟之抽象工厂模式(三) 工厂三兄弟之抽象工厂模式(四) 工厂三兄弟之抽象工厂模式(五) 单例模式-Singleton Pattern 确保对象的唯一性——单例模式 (一) 确保对象的...

    Java实例高难度面试题及解析 - 展现你的编程实力!

    您将了解如何正确创建对象实例、访问实例的成员变量和方法、实现对象的拷贝(包括浅拷贝和深拷贝)、判断对象相等性、管理对象的生命周期、实现线程安全的单例模式等。此外,我们还探讨了对象的哈希码、重写equals()...

    研磨设计模式-part2

    也可以作为高效学生深入学习设计模式的参考读物! 第1章 设计模式基础 第2章 简单工厂 第3章 外观模式 第4章 适配器模式(Adapter) 第5章 单例模式(Singleton) 第6章 工厂方法模式(Factory Method) 第7章...

    研磨设计模式-part4

    也可以作为高效学生深入学习设计模式的参考读物! 第1章 设计模式基础 第2章 简单工厂 第3章 外观模式 第4章 适配器模式(Adapter) 第5章 单例模式(Singleton) 第6章 工厂方法模式(Factory Method) 第7章...

    研磨设计模式-part3

    也可以作为高效学生深入学习设计模式的参考读物! 第1章 设计模式基础 第2章 简单工厂 第3章 外观模式 第4章 适配器模式(Adapter) 第5章 单例模式(Singleton) 第6章 工厂方法模式(Factory Method) 第7章...

    Java-Programmer-Practice:Java工程师成长之路,包含JDK源码解析、Java并发编程、JVM实例解析、Spring全家桶、Spring Cloud以及各类中间件代码实例与教程。欢迎fork、star

    单例模式(饿汉式、懒汉式) 4. Linux的基础知识 Linux下的进程 Linux定时任务调度 Linux的七个运行级别 Linux进程启动顺序 深入理解Linux下的守护进程 理解Linux配置文件 下一阶段更新计划 增加JDK源码的注释 + ...

    java多线程编程_java多线程_

    1.讲解了Java多线程的基础, 包括Thread类的核心API的使用。...6.讲解的单例模式虽然很简单, 但如果遇到多线程将会变得非常麻烦, 如何在多线程中解决这么棘手的问题呢?本章将全面介绍解决方案。

    二十三种设计模式【PDF版】

    设计模式之 Singleton(单态/单件) 阎宏博士讲解:单例(Singleton)模式 保证一个类只有一个实例,并提供一个访问它的全局访问点 设计模式之 Factory(工厂方法和抽象工厂) 使用工厂模式就象使用 new 一样频繁. ...

    Design-Patterns-and-SOLID-Principles-with-Java:Packt发布的Java设计模式和SOLID原理

    相反,您将深入学习所选的设计模式。 在课程中,您还将熟悉SOLID设计和编写简洁代码的主要方面,因为这些概念和应用设计模式应紧密结合。 在课程结束时,您将能够在自己的应用程序中识别(并自信地应用)设计模式,...

    超经典的jdbc学习笔记

    jdbc深入细致的讲解。主要讲解了sql注入。工厂模式的设计。单例模式的设计。以及各种数据库常见sql语句在java面向对象中的编写

    亚信java笔试题-java-design-patterns:设计模式专题,共23种设计模式。GOFdesignpatterns,implem

    每种设计模式均从根本出发,深入理解设计思想,再配合一些小故事加以理解,让设计模式变得通俗易懂。让自己和现在正在阅读的你,能够知其然而知其所以然。 提到单例想到 “饿汉?,懒汉?” 提到工厂想到 “汽车?”...

    龙果java并发编程完整视频

    第16节单例问题与线程安全性深入解析00:27:15分钟 | 第17节理解自旋锁,死锁与重入锁00:24:58分钟 | 第18节深入理解volatile原理与使用00:28:30分钟 | 第19节JDK5提供的原子类的操作以及实现原理00:27:10分钟 | ...

Global site tag (gtag.js) - Google Analytics