目录
一.单例模式的定义
二.单例模式的实现方式
1.懒汉模式:
2.饿汉模式
3.静态内部类方式
4.反射模式
5.枚举方式
6.序列化方式
三.单例模式的应用
文章来源地址https://uudwc.com/A/jAw3A
一.单例模式的定义
保证一个类只有一个实例,并且提供一个全局访问点
使用的场景:重量级的对象、不需要多个实例,以及我们想复用的对象,如线程池,数据库连接池等 等。Spring中的IOC容器是单例对象,JDK的Runtime也是单例对象。
单例模式的类图:
二.单例模式的实现方式
1.懒汉模式:
懒汉模式:延迟加载的方案,只有我们在使用的时候才实例化
先来看看版本1:
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
public class LazySingleton {
private static LazySingleton lazySingleton ;
private LazySingleton() {
}
public static LazySingleton getLazySingleton(){
if(lazySingleton == null){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazySingleton =new LazySingleton();
}
return lazySingleton ;
}
public static void main(String[] args) {
new Thread(()->{
System.out.println(LazySingleton.getLazySingleton());
}).start();
new Thread(()->{
System.out.println(LazySingleton.getLazySingleton());
}).start();
}
}
运行结果:
多个线程的情况,每个线程使用的不是一个对象,这根本就不是一个单例。
先来看看版本2: 将synchronized写在if条件的里面
加锁
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
public class LazySingleton2 {
private static LazySingleton2 lazySingleton ;
private LazySingleton2() {
}
public static LazySingleton2 getLazySingleton(){
if(lazySingleton == null){
synchronized (LazySingleton2.class){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazySingleton =new LazySingleton2();
}
}
return lazySingleton ;
}
public static void main(String[] args) {
new Thread(()->{
System.out.println(LazySingleton2.getLazySingleton());
}).start();
new Thread(()->{
System.out.println(LazySingleton2.getLazySingleton());
}).start();
}
}
运行结果:
依然没有实现单例,为什么呢?
看图,多线程情况下,t1,t2,t3,t4都执行到这里了,假如t1获得了锁,那么进入代码块中新建了一个对象,执行之后释放锁,接着t2竞争到了锁,t2又重新创建了一个对象,这样又不符合单例模式了。
再来看看版本3:将synchronized写在if的外面
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
public class LazySingleton2 {
private static LazySingleton2 lazySingleton ;
private LazySingleton2() {
}
public static LazySingleton2 getLazySingleton(){
synchronized (LazySingleton2.class){
if(lazySingleton == null){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazySingleton =new LazySingleton2();
}
}
return lazySingleton ;
}
public static void main(String[] args) {
new Thread(()->{
System.out.println(LazySingleton2.getLazySingleton());
}).start();
new Thread(()->{
System.out.println(LazySingleton2.getLazySingleton());
}).start();
}
}
执行结果:
这次是只有一个对象了,那么这个版本有没有什么问题呢?
多线程的情况下,每一个线程到这里都得等待锁,这样性能是很低的,
就算单例对象已经产生了,线程依然拿不到这个单例对象,还要等待锁。
所以得在synchronized外面再加一个if判断
继续来看看版本4:将synchronized写在两个if的中间
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
public class LazySingleton3 {
private static LazySingleton3 lazySingleton ;
private LazySingleton3() {
}
public static LazySingleton3 getLazySingleton(){
if(lazySingleton == null) {
synchronized (LazySingleton3.class) {
if (lazySingleton == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazySingleton = new LazySingleton3();
}
}
}
return lazySingleton ;
}
public static void main(String[] args) {
new Thread(()->{
System.out.println(LazySingleton3.getLazySingleton());
}).start();
new Thread(()->{
System.out.println(LazySingleton3.getLazySingleton());
}).start();
}
}
运行结果依然是正确的,确保了单例,这就是单例的DCL模式(Double check lock)
针对这个版本4,还有什么问题呢?
那就是对象创建的过程中的重排序问题
这里还是和jvm基础知识相关,看看学好jvm多么的重要
new一个对象的过程:(这也可能是一道面试题)
1.检查类是否加载
如果没有加载,那么就去加载类
2.为对象在堆中分配一块内存空间
3. 初始化
3.1对象实例变量初始化为0
3.2设置对象头(比如锁状态、对象在Survivor区挺过gc的次数都在这里设置)
3.3调用init<>方法:对象实例变量被赋予程序员想给的值,调用构造函数
4.将内存地址赋给引用这个对象的变量
其中因为即时编译器JIT或者CPU都可能会在物理层面上将第3步和第4步重新排序,
也就是说第4步先执行,第3步后执行 3,4步是没关系的
即:对编译器或者CPU都可能会在物理层面上对字节码进行指令重排
那么lazySingleton = new LazySingleton3();这里就有问题了,多线程的情况下,T1正在获取了锁,正在new一个对象,但是其实这个对象还没有new完,但是引用已经指向该对象的地址,这时候T2来了,判断lazySingleton是不是为空,结果是false,这就导致可能T2线程拿到的是一个半成品的对象,执行就会出错。那怎么办呢?
private volatile static LazySingleton3 lazySingleton ;
将lazySingleton 用volatile 修饰即可
原因:volatile可以阻止lazySingleton 引用对应的对象创建时候的重排序。
那么这个懒汉模式的单例的最终版本最终如下代码所示:
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
public class LazySingletonVolatile {
private volatile static LazySingletonVolatile lazySingleton ;
private LazySingletonVolatile() {
}
public static LazySingletonVolatile getLazySingleton(){
if(lazySingleton == null) {
synchronized (LazySingletonVolatile.class) {
if (lazySingleton == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazySingleton = new LazySingletonVolatile();
}
}
}
return lazySingleton ;
}
public static void main(String[] args) {
new Thread(()->{
System.out.println(LazySingletonVolatile.getLazySingleton());
}).start();
new Thread(()->{
System.out.println(LazySingletonVolatile.getLazySingleton());
}).start();
}
}
2.饿汉模式
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
public class HungrySingleton {
private static HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {
}
private static HungrySingleton getHungrySingleton(){
return singleton ;
}
public static void main(String[] args) {
new Thread(() -> {
System.out.println(HungrySingleton.getHungrySingleton());
}).start();
new Thread(() -> {
System.out.println(HungrySingleton.getHungrySingleton());
}).start();
}
}
饿汉模式是很简洁安全(个人认为)的方式
简洁:代码量少
安全:怎么说呢?难道没有多线程并发问题吗?
这就又涉及到JVM的内存知识了
类加载器的种类:引导类加载器 扩展类加载器 应用程序类加载器 自定义类加载器(稍稍复习一下)
类加载的过程:
1.编译器编译完成的.class二进制数据文件被类加载器加入到JVM的数据运行区的方法区,
之后在堆区生成class对象
2.连接
2.1.验证:比如是不是开头是cafe babe,
也就是查看被加载的二进制数据文件是不是符合JVM的规范
2.2.准备:类的static静态属性赋默认值 int类型赋0 布尔类型赋false 引用类型赋null
2.3.解析:静态解析,常连池中的符号引用转换成一个直接引用
3.初始化:类的static静态属性赋程序员想给赋的值,当然别忘了静态代码块中的内容也在这时候执行
要说的两点是:
第一:类加载是先于new对象执行的
第二:类加载过程是线程安全的,原子性的,是JVM帮助我们实现线程安全的一种机制。
理解了上述说的,那一定知道了为啥饿汉模式是线程安全的了,类加载之后这个对象就有了,而且JVM帮助我们线程安全的实现了创建对象的过程。
饿汉模式是推荐使用的创建单例模式的方式。
3.静态内部类方式
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
/**
* 静态内部类的方式创建单例模式
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}
public static StaticInnerClassSingleton getSingleton(){
return InnerClassSingletonHolder.singleton ;
}
public static class InnerClassSingletonHolder{
private static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(StaticInnerClassSingleton.getSingleton());
}).start();
}
}
}
思考:静态内部类InnerClassSingletonHolder何时被加载呢?
是我们在 返回InnerClassSingletonHolder.singleton (使用)的时候进行内部类的加载,这个过程也是通过JVM类加载的过程保证线程安全。
只有在真正使用对应的类的时候,才会触发初始化,也可以说是类加载,例如:当前类是启动类即main函数所在的类,直接进行new操作,访问静态属性,访问静态方法,用反射访问类,初始化一个类的子类等。
4.反射模式
这里其实不能说是一种创建单例模式的方式,而是一种反射攻击
以静态内部类创建单例为例子,进行 反射测试
重点看main方法:
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 静态内部类的方式创建单例模式
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}
public static StaticInnerClassSingleton getSingleton(){
return InnerClassSingletonHolder.singleton ;
}
public static class InnerClassSingletonHolder{
private static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<StaticInnerClassSingleton> constructor
= StaticInnerClassSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
StaticInnerClassSingleton staticInnerClassSingleton = constructor.newInstance();
StaticInnerClassSingleton singleton = StaticInnerClassSingleton.getSingleton();
System.out.println(staticInnerClassSingleton == singleton);
}
}
结果:false (间接说明了反射生成的是新对象)。
这样又破坏了单例模式, 怎么防呢?
在饿汉模式或者静态内部类的模式下,在构造方法中加判断:
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 静态内部类的方式创建单例模式
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
if(InnerClassSingletonHolder.singleton!=null){
throw new RuntimeException("单例模式不允许多个实例...");
}
}
public static StaticInnerClassSingleton getSingleton(){
return InnerClassSingletonHolder.singleton ;
}
public static class InnerClassSingletonHolder{
private static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<StaticInnerClassSingleton> constructor
= StaticInnerClassSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
StaticInnerClassSingleton staticInnerClassSingleton = constructor.newInstance();
System.out.println(staticInnerClassSingleton);
}
}
结果:
5.枚举方式
点进constructor.newInstance();方法:
其中的这块代码:说明如果反射创建对象的类型是枚举类型的话,jdk会帮助我们,不让反射的方式进行对象的创建,也就是防止了反射攻击。
验证一下:
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum EnumSingleton {
INSTANCE ;
public void say(){
System.out.println(this.hashCode());
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingleton instance = constructor.newInstance("INSTANCE", 0);
instance.say();
}
}
运行结果:
Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
所以说枚举类型帮助我们阻止了反射攻击。
看字节码的话,我们就会发现其中的奥秘:枚举类型底层其实也是一种饿汉模式
使用javap 命令:
javap -v -p F:\enjoystudy\concurrent\out\production\concurrent\com\tuling\learnjuc\sync\xiaoshanshan\singleton_study\EnumSingleton.class
查看字节码:
可以看到com.tuling.learnjuc.sync.xiaoshanshan.singleton_study.EnumSingleton这个类是继承了java.lang.Enum这个抽象类
而且构造方法是私有的
具体的new操作在一个static代码块中执行的。
6.序列化方式
package com.tuling.learnjuc.sync.xiaoshanshan.singleton_study;
import java.io.*;
public class SingletonSerializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 写出 序列化
// 此处以饿汉模式为例 而且饿汉类已经实现了Serializable接口
HungrySingleton instance = HungrySingleton.getHungrySingleton();
ObjectOutputStream objectOutputStream
= new ObjectOutputStream(new FileOutputStream("objectInputStream"));
objectOutputStream.writeObject(instance);
objectOutputStream.close();
// 写入 反序列化
ObjectInputStream objectInputStream
= new ObjectInputStream(new FileInputStream("objectInputStream"));
HungrySingleton o = (HungrySingleton)objectInputStream.readObject();
System.out.println(instance == o);
objectInputStream.close();
}
}
运行结果:false
这说明 反序列化已经破坏了单例模式
那怎么办呢?
在Serializable接口中有这么一段话:就是说从流中读取的对象对应的类应该有一个方法:任意的访问修饰符来修饰的方法
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
在懒汉的单例类加上这个方法:
ps:这里还要做的一件事就是:懒汉单例类中 要加上序列版本号,为什么呢?
比如:第一步,把懒汉单例类对象序列化了
第二步,把懒汉单例类中加入了新的方法或者新的属性
第三步,把懒汉单例类对象进行反序列化
结果就报错了:
版本号不兼容
所以要加上版本号
再测试就是true了。
以上是针对饿汉模式,其实懒汉模式和静态内部类都会被反序列化这样的情况破坏掉单例模式,解决方法如上。
但是针对枚举类型的单例模型,就不会被反序列化破坏掉单例模式。
看看为什么?主要看看这个方法
objectInputStream.readObject();
F5继续:
Class和Enum处理的方式不一样的,在TC_OBJECT中会检查是不是有readResolve这个方法签名,这个方法就是我们上面要加的方法。
三.单例模式的应用
文章来源:https://uudwc.com/A/jAw3A