注: 看一遍只能有个印象,想学会还要自己敲一遍(第四节反射选看)
目录标题
1. 什么是单例模式?
- 单例模式强调一个单字,保证每个类仅有一个实例,访问这个类的线程共用一个对象。
2. 看看它的几种实现方式
2.1 最简单的饿汉式
饿汉式,顾名思义,它很“饿”啊,就想一口吃个胖子,很急,在类加载的时候就创建了实例对象,如下方代码所示,还没有调用getInstance方法,实例对象就给你准备好了,你说急不急?
public class Hungry {
//私有构造器,避免外部创建对象
private Hungry(){}
//实例对象(类加载的时候就创建了实例对象)
private static final Hungry HUNGRY = new Hungry();
//获取单例的方法
public static Hungry getInstance(){
return HUNGRY;
}
}
- 它的弊端也很明显,在类加载的时候就实例化了对象,浪费内存,线程也不安全。
- 为了解决这个不在类加载的时候就初始化对象的问题,就出现了下面的懒汉式,我们接着看。
3. 懒汉式
3.1 简单的懒汉式
下面的代码解决了饿汉式浪费内存的问题,但是线程还是不安全的,我们在3.2节中进行线程安全改进。
public class Lazy01 {
//私有构造器
private Lazy01(){
}
//没有实例化
private static Lazy01 LAZY ;
public static Lazy01 getInstance(){
if(LAZY == null){
LAZY = new Lazy01();
}
return LAZY;
}
}
- 你说线程不安全就不安全?我非要检测一下试试!
public class Lazy01 {
//私有构造器
private Lazy01(){
System.out.println(Thread.currentThread().getName() + "获取实例");
}
//没有实例化
private static Lazy01 LAZY ;
public static Lazy01 getInstance(){
if(LAZY == null){
LAZY = new Lazy01();
}
return LAZY;
}
public static void main(String[] args) {
//创建10个线程,来获取单例类的实例
for(int i = 0;i < 10;i++){
new Thread(()->Lazy01.getInstance()).start();
}
}
}
我们看输出,两次输出不相同,线程不安全
3.2 解决线程安全问题的懒汉式
我们在 getInstance()方法上,添加synchronized关键字,就解决了线程安全
public static synchronized Lazy01 getInstance(){
if(LAZY == null){
LAZY = new Lazy01();
}
return LAZY;
}
- 但是我们发现,用了 synchronized 关键字修饰,效率很慢,为了解决效率很慢,我们又引入了双重检测锁的懒汉式。
3.3 双重检测锁 DLC懒汉式
我们加了一个双重检测锁,保证了单一线程创建实例,又提高了效率
public static Lazy01 getInstance(){
//LAZY == null的时候把这个类锁起来,保证只有一个线程访问
if(LAZY == null){
synchronized (Lazy01.class){
//这里再创建实例
if(LAZY == null){
LAZY = new Lazy01();
}
}
}
return LAZY;
}
- 但是你以为加了双重检测锁就完了嘛?太天真了吧
- 我们还需要做一点儿小细节的改进。
LAZY = new Lazy01(); // 创建对象的过程并不是原子性的,他的底层有三个步骤
// 1. 分配内存空间
// 2. 执行构造方法,初始化实例对象
// 3. 将对象指向这块儿空间
若底层按照正常的顺序,123执行的话,没有问题
假如没有按照123,而是132这种情况,我们想一下
线程A 按照132创建实例对象
同时呢,线程B 也进来了,当A执行到3这个步骤的时候,还没有进行初始化对象,但是已经指向了分配的内存空间
但是!问题来了!
B线程会认为 LAZY 不为空,直接 return,那么这就返回了一个没有初始化的LAZY对象。
解决方法也很简单,加上一个volatile修饰,如下
private static volatile Lazy01 LAZY;
- 利用静态内部类,也可以实现双重锁的功能
3.4 静态内部类实现
这种情况是,只有显示的调用 getInstance() 方法的时候,才能对内部类进行加载,否则也是不进行初始化的
public class Lazy02 {
//私有无参构造
private Lazy02(){
}
private static class InnerLazy{
private static final Lazy02 LAZY = new Lazy02();
}
public static Lazy02 getInstance(){
return InnerLazy.LAZY;
}
}
- 但是以上介绍的都是能被反射破解的,下面我们再简单聊聊反射破解吧
4. 反射破解DLC懒汉式单例(选看)
import java.lang.reflect.Constructor;
public class Lazy01 {
//私有构造器
private Lazy01(){
}
//没有实例化
private static volatile Lazy01 LAZY ;
public static Lazy01 getInstance(){
//LAZY == null的时候把这个类锁起来,保证只有一个线程访问
if(LAZY == null){
synchronized (Lazy01.class){
//这里再创建实例
if(LAZY == null){
LAZY = new Lazy01();
}
}
}
return LAZY;
}
//重点看这一部分代码!!!
public static void main(String[] args) throws Exception {
//正常创建对象
Lazy01 lazy01 = Lazy01.getInstance();
//获取无参构造
Constructor<Lazy01> declaredConstructor = Lazy01.class.getDeclaredConstructor();
//破坏构造函数的私有性
declaredConstructor.setAccessible(true);
//通过反射创建对象
Lazy01 lazy02 = declaredConstructor.newInstance(null);
//下面我们打印一下,看看它是否是一个对象
System.out.println(lazy01);
System.out.println(lazy02);
}
}
我们可以发现呐,打印的是两个不同的对象,这就违背了单例模式的原则!
- 那我们如何解决呢?
4.1 再加一把锁来解决反射问题
我们在私有构造器下,加上如下代码
//私有构造器
private Lazy01(){
synchronized (Lazy01.class){
if(LAZY != null){
throw new RuntimeException("不要试图使用反射来创建对象");
}
}
}
当我们再运行反射创建对象的时候,就会报错
- 但是如果我们两个对象都是通过反射创建的呢?又出现问题了。我们接着看。
4.2 用反射创建两个对象又出现了问题
这里我们不调用 getInstance() 方法创建对象,而是都用反射,我们来看代码
public static void main(String[] args) throws Exception {
//正常创建对象
//Lazy01 lazy01 = Lazy01.getInstance();
//获取无参构造
Constructor<Lazy01> declaredConstructor = Lazy01.class.getDeclaredConstructor();
//破坏构造函数的私有性
declaredConstructor.setAccessible(true);
//通过反射创建对象
Lazy01 lazy02 = declaredConstructor.newInstance(null);
Lazy01 lazy03 = declaredConstructor.newInstance(null);
//下面我们打印一下,看看它是否是一个对象
//System.out.println(lazy01);
//用两次反射创建对象
System.out.println(lazy02);
System.out.println(lazy03);
}
我们可以发现,它又能创建两个不同的对象了
- 那么这个又怎么解决呢???
4.3 解决4.2出现的问题
我们添加一个参数进行判断,下面是对代码的修改
//我们再这里加上一个flag来判断
private static boolean flag = false;
//私有构造器
private Lazy01(){
synchronized (Lazy01.class){
//再没有创建对象的时候为false,创建过了就为true,这样来阻止对象创建
if(flag == false){
flag = true;
}else {
throw new RuntimeException("不要试图使用反射来创建对象");
}
}
}
我们再来测试一下,发现可行了
- 但是,这样就真的解决了反射的问题吗?实际上没有
4.4 反射问题的再次出现
假设我们知道了flag字段的名字,能够通过反射进行获取
public static void main(String[] args) throws Exception {
//正常创建对象
//Lazy01 lazy01 = Lazy01.getInstance();
//获取无参构造
Constructor<Lazy01> declaredConstructor = Lazy01.class.getDeclaredConstructor();
//破坏构造函数的私有性
declaredConstructor.setAccessible(true);
//第一次用反射创建对象
Lazy01 lazy02 = declaredConstructor.newInstance(null);
//假设我们知道了字段的名字,来获取这个字段
Field flag = Lazy01.class.getDeclaredField("flag");
//破坏字段的私有性质
flag.setAccessible(true);
//创建完成一次对象后,把flag的值又改为false
flag.set(lazy02,false);
//第二次用反射创建对象
Lazy01 lazy03 = declaredConstructor.newInstance(null);
//下面我们打印一下,看看它是否是一个对象
//System.out.println(lazy01);
System.out.println(lazy02);
System.out.println(lazy03);
}
- 那这样下去,岂不是道高一尺魔高一丈,就停不下来了???
- No,不是的,我们还有最终技能,枚举
5. 枚举单例模式
枚举类解决了反射的问题
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
- 好了,到这里我们就说完了,希望你能有收获,有问题也欢迎指出