欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java深入学习之单例模式

程序员文章站 2022-07-15 13:59:23
...
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();
}