题引:
总结自己学习Java常用设计模式后的理解,本篇为单例模式。
未知的事物往往令人不知所措,为了透彻理解单例模式,我们必须知道:
什么是Java中的单例模式?
百度百科给出了Java中单例模式概念的内涵,Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
由定义我们可以很清晰的抽象出:
实现Java单例模式类有哪些通用设计规则?
(1)私有化类构造器。
(2)定义静态私有的类对象。
(3)提供公共静态的获取该私有类对象的方法。
了解了单例模式的概念,以及单例模式的通用设计规则,对于如何实现一个Java单例,相信我们已不再感觉束手无策。但在了解具体实现之前,我们还需要了解:
Java单例模式解决了什么问题?
Java的单例模式主要解决了多线程并发访问共享资源的线程安全问题。
Java单例模式主要应用场景有哪些?
1.共享资源的访问与操作场景,如Windows系统的资源管理器,Windows系统的回收站,显卡的驱动程序,系统的配置文件,工厂本身(类模板),应用程序的日志对象等。
2.控制资源访问与操作的场景,如数据库的连接池,Java的线程池等。
了解了Java单例模式出现的缘由以及出现的场合,那么,Java的单例究竟是以怎样的方式出现在这些场合中呢?
Java中单例模式的常用实现方式有哪些?
本文主要介绍4中常用的Java单例实现方式:饿汉式、懒汉式、注册登记式、序列化与反序列化式。
饿汉式
饿汉式,顾名思义,就是指在JVM首次访问到该单例类时,就会把该单例类对象创建出来,并保存在内存中,不管后续是否会使用到这个单例。
/**
* @author jiangsj
* 饿汉式单例
* 优点: (1)没有加任何的锁,执行效率较高
* (2)线程安全
* 缺点: 类加载的时候就初始化,后续不一定会使用该实例,导致内存浪费
*/
public class HungrySingleton {
// 1.私有化类构造器
private HungrySingleton() {}
// 2.定义静态私有的类对象
private static HungrySingleton instance = new HungrySingleton();
// 3.提供公共静态的获取该私有类对象的方法
public static HungrySingleton getInstance() {
return instance;
}
}
懒汉式
懒汉式,相比于饿汉式,它是指JVM首次访问到该单例类时,并不会实例化该单例类对象,只有等到后续被外部调用时,才会实例化该单例类对象。
饿汉式的写法比较固定,懒汉式由于延时加载的特性,写法上有一些变化,一般来说,懒汉式存在4个变种。
懒汉式1
/**
* @author jiangsj
* 懒汉式1单例
* 优点: 由于懒汉式延时加载特性,使用该实例时才实例化,节省了内存资源
* 缺点: (1)该种实现方式存在线程不安全问题
* (2)反序列化,反射与克隆可破坏单例
*/
public class LazySingleton1 {
// 1.私有化类构造器
private LazySingleton1() {}
// 2.定义静态私有的类对象
private static LazySingleton1 instance = null;
// 3.提供公共静态的获取该私有类对象的方法
public static LazySingleton1 getInstance() {
if (instance == null) {
instance = new LazySingleton1();
}
return instance;
}
}
懒汉式2
/**
* @author jiangsj
* 懒汉式2单例
* 优点: (1)由于懒汉式延时加载特性,使用该实例时才实例化,节省了内存资源
* (2)线程安全
* 缺点: (1)给获取实例的公共方法加上同步锁synchronized,性能受到影响
* (2)反序列化,反射与克隆可破坏单例
*/
public class LazySingleton2 {
// 1.私有化类构造器
private LazySingleton2() {}
// 2.定义静态私有的类对象
private static LazySingleton2 instance = null;
// 3.提供公共静态的获取该私有类对象的方法,为了线程安全,给方法加上同步锁synchronized
public static synchronized LazySingleton2 getInstance() {
if (instance == null) {
instance = new LazySingleton2();
}
return instance;
}
}
懒汉式3——双重锁检查
/**
* @author jiangsj
* 懒汉式3——双重锁检查单例
* 优点: (1)由于懒汉式延时加载特性,使用该实例时才实例化,节省了内存资源
* (2)线程安全
* 缺点: (1)如果不加volatile关键词防止指令重排,双重锁检查单例可能会出现不完整实例
* (2)反序列化,反射与克隆可破坏单例
*/
public class LazySingleton3 {
// 1.私有化类构造器
private LazySingleton3() {}
// 2.定义静态私有的类对象,为了防止出现不完整实例,加了防止指令重排的volatile关键字
private static volatile LazySingleton3 instance = null;
// 3.提供公共静态的获取该私有类对象的方法,为了线程安全,做了双重锁检查机制
public static LazySingleton3 getInstance() {
if (instance == null) {
synchronized (LazySingleton3.class) {
if (instance == null) {
instance = new LazySingleton3();
}
}
}
return instance;
}
}
懒汉式4——静态内部类
/**
* @author jiangsj
* 懒汉式4——静态内部类单例
* 优点: (1)由于懒汉式延时加载特性,使用该实例时才实例化,节省了内存资源
* (2)在外部类被调用的时候内部类才会被加载,内部类要在方法调用之前初始化,巧妙地避免了线程安全问题
* (3)兼顾了synchronized的性能问题
* 缺点: 反序列化,反射与克隆可破坏单例
*/
public class LazySingleton4 {
private boolean initialized = false;
// 1.私有化类构造器
private LazySingleton4() {
synchronized (LazySingleton4.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("单例已被侵犯");
}
}
}
// 2.定义静态私有的类对象
private static class LazyHolder {
private static final LazySingleton4 INSTANCE = new LazySingleton4();
}
// 3.提供公共静态的获取该私有类对象的方法
public static final LazySingleton4 getInstance() {
return LazyHolder.INSTANCE;
}
}
注册登记式
每使用一次,都往一个固定的容器中去注册并将使用过的对象进行缓存,下次去取对象的时候,就直接从缓存中取值,以保证每次获取的都是同一个对象。Spring IOC中的单例模式,就是典型的注册登记式单例。
/**
* @author jiangsj
* 注册登记式——map容器单例
*/
public class RegisterSingletonMap {
// 1.私有化类构造器
private RegisterSingletonMap() {}
// 2.定义静态私有的类对象容器
private static Map<String, RegisterSingletonMap> map = new ConcurrentHashMap<String, RegisterSingletonMap>();
// 3.提供公共静态的获取该私有类对象的方法
public static RegisterSingletonMap getInstance(String name) {
String className = RegisterSingletonMap.class.getName();
if (!className.equals(name)) {
name = className;
}
synchronized (RegisterSingletonMap.class) {
if (!map.containsKey(name)) {
map.put(name, new RegisterSingletonMap());
}
}
return map.get(name);
}
}
注册登记式还有一种写法,枚举型单例
/**
* @author jiangsj
* 注册登记式——枚举单例
*/
public class RegisterSingletonEnum {
// 1.私有化类构造器
private RegisterSingletonEnum() {}
// 2.提供公共静态的获取该私有类对象的方法
public static RegisterSingletonEnum getInstance() {
return Singleton.INSTANCE.getInstance();
}
// 3.定义静态私有的枚举对象,利用枚举特性获得类的单实例
private static enum Singleton {
INSTANCE;
private RegisterSingletonEnum singleton;
// JVM保证此方法只调用一次
private Singleton() {
singleton = new RegisterSingletonEnum();
}
public RegisterSingletonEnum getInstance() {
return singleton;
}
}
}
序列化与反序列化式
序列化与反序列化式单例,需要重写readResolve()方法。
/**
* @author jiangsj
* 序列化与反序列化式单例
*/
public class SeriableSingleton implements Serializable {
// 1.私有化类构造器
private SeriableSingleton() {}
// 2.定义静态私有的类对象
private static final SeriableSingleton INSTANCE = new SeriableSingleton();
// 3.提供公共静态的获取该私有类对象的方法
public static SeriableSingleton getInstance() {
return INSTANCE;
}
// 4.重写readResolve()方法,保证反序列化生成对象时获得的是同一个对象
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
序列化与反序列化式单例的测试类
/**
* @author jiangsj
* 序列化与反序列化式单例测试类
*/
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = SeriableSingleton.getInstance();
SeriableSingleton s2 = null;
// 序列化,持久化到磁盘
try {
FileOutputStream fos = new FileOutputStream(new File("SeriableSingleton.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 从磁盘反序列化,重新生成对象
try {
FileInputStream fis = new FileInputStream(new File("SeriableSingleton.txt"));
ObjectInputStream ois = new ObjectInputStream(fis);
s2 = (SeriableSingleton) ois.readObject();
ois.close();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("序列化时对象 s1 : " + s1);
System.out.println("反序列化后对象 s2 : " + s2);
System.out.println(s1 == s2);
}
}
测试结果如下
E:\tts\JDK\JDK1.8\bin\java "-javaagent:E:\Intellij IDEA\IntelliJ IDEA 2017.3\lib\idea_rt.jar=49947:E:\Intellij IDEA\IntelliJ IDEA 2017.3\bin" -Dfile.encoding=UTF-8 -classpath "E:\tts\JDK\JDK1.8\jre\lib\charsets.jar;E:\tts\JDK\JDK1.8\jre\lib\deploy.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\access-bridge-64.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\cldrdata.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\dnsns.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\jaccess.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\jfxrt.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\localedata.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\nashorn.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\sunec.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\sunjce_provider.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\sunmscapi.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\sunpkcs11.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\zipfs.jar;E:\tts\JDK\JDK1.8\jre\lib\javaws.jar;E:\tts\JDK\JDK1.8\jre\lib\jce.jar;E:\tts\JDK\JDK1.8\jre\lib\jfr.jar;E:\tts\JDK\JDK1.8\jre\lib\jfxswt.jar;E:\tts\JDK\JDK1.8\jre\lib\jsse.jar;E:\tts\JDK\JDK1.8\jre\lib\management-agent.jar;E:\tts\JDK\JDK1.8\jre\lib\plugin.jar;E:\tts\JDK\JDK1.8\jre\lib\resources.jar;E:\tts\JDK\JDK1.8\jre\lib\rt.jar;E:\Intellij IDEA\idea_workspace\SpringStudyProject\out\production\SpringStudyProject" study.patterns.singleton.test.SeriableSingletonTest
序列化时对象 s1 : study.patterns.singleton.seriable.SeriableSingleton@135fbaa4
反序列化后对象 s2 : study.patterns.singleton.seriable.SeriableSingleton@135fbaa4
true
Process finished with exit code 0
饿汉式、懒汉式、注册登记式单例的测试类
/**
* @author jiangsj
*/
public class SingletonTest {
public static void main(String[] args) {
// 模拟并发的线程数
int count = 200;
// 发令枪
CountDownLatch latch = new CountDownLatch(count);
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
new Thread(){
@Override
public void run() {
try{
try {
// 阻塞,count = 0 就会释放所有的共享锁,模拟多线程并发
latch.await();
} catch (Exception e){
e.printStackTrace();
}
/*
* 可能会有很多线程同时去访问getInstance()
* 这里的RegisterSingletonEnum可替换成饿汉式、懒汉式、注册登记式的单例类来进行测试
*/
RegisterSingletonEnum obj = RegisterSingletonEnum.getInstance();
System.out.println(System.currentTimeMillis() + ":" + obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
// 每循环一次,就启动一个线程,每次启动一个线程,count --
latch.countDown();
}
long end = System.currentTimeMillis();
// 让主线程延时,该延时期间内,模拟的多线程会去并发访问获取单例的方法
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("总耗时:" + (end - start));
}
}
结束语
以上便是我对Java单例模式学习的总结,如有疏漏或错误之处,欢迎大家留言指正。
网友评论