单例模式 Java单例实现

单例模式:保证一个类只有一个实例,并且提供一个访问该类实例的全局访问点。

优点:

  • 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永驻留内存的方式来解决
  • 单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

单例实现要尽可能实现 线程安全 效率高 懒加载

常见的五种单例模式的实现方式:

主要分类

  • 懒汉 (线程安全 调用效率不高,但是可以延时加载)
  • 饿汉 (线程安全 调用效率高, 但是不能延时加载)

其他分类

  • 双重检测锁式 (由于JVM底部内存模型问题,偶尔会出现问题。不建议使用)
  • 静 态 内 部 类(线程安全,调用效率高,但是,不可以延时加载)
  • 枚 举 类 型 (线程安全,调用效率高, 不能延时加载)

单例实现

1、饿汉式(单例对象立即加载)

package feng.com.GOF;

/**
 * 单例模式实现  饿汉式
 * @author Administrator
 *
 */

public class SingletonDemo01 {

	// 类加载是天然线程安全的的
	private static SingletonDemo01 single1 = new SingletonDemo01();	//类初始化时立刻加载这个对象
	
	private SingletonDemo01() {};	// 私有化构造器
	
	public static SingletonDemo01 getInstance() {
		return single1;
	}
	
}

static变量会在类加载时初始化,且一个类只会被加载一次,也就保证了single1实例是唯一的。但是如果只是加载本类,不调用getInstance()就会造成浪费,因为不管调用与否,实例single1 都会被创建。 构造器私有化,其他类中获取实例只能通过getInstance()方法获取对象实例。

2、懒汉式(单例对象可延时加载)

package feng.com.GOF;

/**
 * 单例实现 懒汉式
 * @author feng
 *
 */

public class SingletonDemo02 {

	private static SingletonDemo02 single2;		//未初始化实例对象

	private SingletonDemo02() {};				//私有化构造方法
	
	public static synchronized SingletonDemo02 getInstance() {
		if(single2 == null) {
			single2 = new SingletonDemo02();	//真正使用时才会创建对象
		}
		return single2;
	}
	
}

懒汉式(懒加载/延时加载),真正调用时再加载实例,延时加载。 但是资源利用率太低了。每次调用getInstance()方法都需要同步(不同步的话,可能多个线程同时调用该方法,导致生成了不止一个实例),并发效率低。

3、双重检测锁机制
网络看到过一个双重锁代码实现

package feng.com.GOF;

public class SingletonDemo03 {

	private static SingletonDemo03 single3;
	
	private SingletonDemo03() {};
	
	public static SingletonDemo03 getInstance() {
		
		if(single3 == null) {
			SingletonDemo03 tempInstance;
			synchronized (SingletonDemo03.class) {		
				tempInstance = single3;
				if(tempInstance == null) {
					// 我也不太明白第一个同步锁内 第二个同步锁有什么意义。。。
					synchronized (SingletonDemo03.class) {
						if(tempInstance == null) {
							tempInstance = new SingletonDemo03();
						}
					}
					single3 = tempInstance;
				}
			}
		}
		
		return single3;
	}
}

就个人来说,已经加过一个同步锁了,内部的锁还有意义没有,锁学的不是很好,不太明白,感觉是没什么意义。但这代码确实有。。。求大神指教。。。

如果按个人理解,双重检测锁 并不是两个锁的意思吧。。。而是在锁上加两层检测来保证实例只会创建一次。

package feng.com.GOF;

public class SingletonDemo03_1 {
	
	private static SingletonDemo03_1 single3;
	
	private SingletonDemo03_1() {};
	
	public static SingletonDemo03_1 getInstance() {
		
		if(single3 == null) {		// 判断实例是否为空 只有第一次加载该实例时会执行下列代码块
			synchronized (SingletonDemo03_1.class) {	// 加锁
				if(single3 == null) {	// 二次判断的目的是可能有线程先获取到同步锁。已经生成了实例对象
					single3 = new SingletonDemo03_1();	// 生成实例对象
				}
			}
		}
		
		return single3;
	}

}

single3是静态变量,放在静态区中,也就意味着该变量是所有线程共有的。这才是进行二次判断保证线程不重复创建实例的依据。 双重检测将同步内容放置到了if内部,提高执行效率,不必每次获取对象时都需同步,只有第一次才同步创建,之后就没不要了。
据说 由于编译器优化和JVM底层内部模型原因,偶尔会出现问题,不建议使用。(咱也不懂,也不知道上哪问。。。会出现什么问题。。。也有说这个方法好的)

4、静态内部类实现方式
静态内部类不会再外部类初始化时初始化,而是调用时再初始化,可以看做顶级类。

package feng.com.GOF;

/**
 * 静态内部类实现单例
 * @author feng
 *
 */

public class SingletonDemo04 {
	
	private SingletonDemo04() {
		
	}
	
	// 特别注意  外部类初始化的时候不会初始化静态内部类。
	private static class SingletonClassInstance{
		private static final SingletonDemo04 single04 = new SingletonDemo04();
	}
	
	public static SingletonDemo04 getInstance() {
		return SingletonClassInstance.single04;
	}
	
}

静态内部类不会立即初始化保证了懒加载。single4是static final 类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,类加载过程是天生线程安全的,从而保证了线程安全性。兼备了高效并发和懒加载的优势

5、枚举方式实现单例模式

package feng.com.GOF;

/**
 * 枚举方式实现单例
 * @author feng
 *
 */

public enum SingletonDemo05 {

	// 这个枚举元素 本身就是单例对象
	INSTANCE;
	
	// 可以添加自己需要的操作
	public void singletonOperation() {
		//System.out.println("输出测试");
	}
}

枚举实现简单,可能理解起来比较困难。。。枚举本身就是单例模式,有JVM从根本上提供保障!避免通过反射和反序列化漏洞。 不过不能延时加载。

简单测试 运行效率

package com.feng.singleton;

import java.util.concurrent.CountDownLatch;

/**
 * 测试多线程环境下五种单例模式的效率
 * @author feng
 *
 */

public class Client3 {

	public static void main(String[] args) throws Exception {
		
		long start = System.currentTimeMillis();
		
		// CountDownLatch 线程辅助类   可以在线程运行期间等待其他线程执行完毕
		int threadNum = 10;
		final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
		
		for(int i= 0; i<10; i++) {
		
			new Thread(new Runnable() {
				public void run() {
					for(int j=0; j<100000; j++) {
						Object o = SingletonDemo01.getInstance();
					}
					countDownLatch.countDown();
				}
			}).start();
			
		}
		
		
		// main 线程阻塞,直到计数器变为0 才会继续向下执行
		countDownLatch.await(); 	// 等待阻塞 本质就是循环检测
		
		long end = System.currentTimeMillis();
		System.out.println("总耗时:"+(end-start));
		
	}
	
}

关于选择:
如果没有懒加载要求,那么枚举优于饿汉式。
如果有懒加载需求, 那么静态内部类优于懒汉式。
双重检测锁我也不知道好不好。。。

以下内容了解知道就行 不必深究 我也不懂 大神知道带我飞

单例模式的问题与测试

1、反射破解单例(不可破解枚举)以懒汉式加载为例。一般用不到。
单例类

package com.feng.singleton;

public class SingletonDemo06 {
	
	private static SingletonDemo06 single6;

	private SingletonDemo06() {	};
	
	public static synchronized SingletonDemo06 getInstance() {
		if(single6 == null) {
			single6 = new SingletonDemo06();
		}
		return single6;
	}
}

反射破解单例

package com.feng.singleton;

import java.lang.reflect.Constructor;

/**
 *   测试反射和反序列化破解单例模式
 * @author feng
 *
 */

public class Client2 {

	public static void main(String[] args) throws Exception {
		SingletonDemo06 s6_1 = SingletonDemo06.getInstance();
		SingletonDemo06 s6_2 = SingletonDemo06.getInstance();
		
		System.out.println(s6_1);
		System.out.println(s6_2);
		
		// 获得Class 对象
		Class<SingletonDemo06> clazz = (Class<SingletonDemo06>) Class.forName("com.feng.singleton.SingletonDemo06");
		
		// 获得构造器
		Constructor<SingletonDemo06> constructor = clazz.getDeclaredConstructor(null);		
		constructor.setAccessible(true); 	// 跳过权限检查,也就可以访问私有变量了
		
		SingletonDemo06 s6_3 = constructor.newInstance();
		SingletonDemo06 s6_4 = constructor.newInstance();
		System.out.println(s6_3);
		System.out.println(s6_4);
		System.out.println(s6_3 == s6_4);
	}
}

constructor.setAccessible(true); // 跳过权限检查,也就可以访问私有变量了。这是关键。这样s6_3与s6_4就不是同一个对象了
防反射破解单例可通过在构造方法种手动抛出异常 谁想出来的。优秀。。。

private SingletonDemo06() {
	if(single6 != null) {
		throw new RuntimeException();
	}
};

这样反射就无法破解了。因为第二次创建对象时会报错。

通过序列化反序列化 生成新的对象。。。

	// 序列化
	FileOutputStream fos = new FileOutputStream("e:/a.txt");
	ObjectOutputStream oos = new ObjectOutputStream(fos);
	oos.writeObject(s6_1);
	oos.close();
	
	// 反序列化
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:/a.txt"));
	SingletonDemo06 s6_5 = (SingletonDemo06) ois.readObject();
	
	System.out.println(s6_5);

不过序列化反序列化需要SingletonDemo06实现Serializable接口。

public class SingletonDemo06 implements Serializable 

这样s6_1与s6_2是同一个对象, 单s6_5是不同的。

防止序列化反序列化操作需在SingletonDemo06中重写一个方法 readResolve()

// 反序列化时直接调用该方法返回指定对象,而不会读取序列化文件产生新对象返回
public Object readResolve() throws ObjectStreamException{
	return single6;
}

至于为什么要实现 Serializable 接口,不实现不就好了吗。。。我也不懂。

发布了59 篇原创文章 · 获赞 11 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/dandanfengyun/article/details/95374502