【原创】从windows回收站谈单例

2020-12-17 15:32

阅读:771

标签:volatil   href   ref   饿汉   准备   也会   构造方法   name   带来   

【原创】从windows回收站谈单例

点击上方“java进阶架构师”,选择右上角“置顶公众号”
20大进阶架构专题每日送达
技术图片
顾名思义,单例模式指的是确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。隐藏其所有的构造方法。

对于有些类而已,咱们需要确保对象的唯一性。举个大家熟悉的列子--Windows 的回收站,正常来讲,点击“回收站”图标,会弹出一个类似下图的界面。可是不管你重复上述操作多少次,Windows 系统始终只会弹出一个回收站的窗口,也就是说在一个 Windows 系统中,回收站存在唯一性。
技术图片

其实我们可以想一想,为什么会这样设计呢?

首先,如果能弹出多个窗口,那这些窗口的数据是否应该完全一致呢?从用户的角度来讲,用户肯定是要求一致的,比如一号窗口显示回收站里有两个文件,二号窗口显示只有一个,到底哪个才是真实的呢?这会给用户带来误解,作为用户,想必大家都不愿接受。

其次,如果弹出的窗口内容完全一致,全部是重复信息,那么完全没必要显示多个同步的窗口。而且在二号窗口做的改变需要同步到一号窗口里,势必也会浪费一定的系统资源。

回到实际开发中,下面我们来模拟实现 Windows 回收站。
技术图片

为了实现 Windows 回收站的唯一性,我们得进行如下步骤来对该类进行重构。

首先,由于每次使用 new 关键字来实例化 RecycleBin 类时,都会产生一个新对象,为了确保实例的唯一性,我们得将 RecycleBin 的构造函数可见性改为 private。

private RecycleBin() {} //初始化窗口
其次,虽然将可见性改为 private,可是内部还是可以创建的,因此我们需要定义一个静态的私有成员变量。

private static RecycleBin recycle = null;
为了保证封装性,我们得把私有成员变量也改为 private,但外界该如何使用它呢?答案是增加一个公有的静态方法。

public static RecycleBin getInstance(){
if (recycle == null){
recycle = new RecycleBin();
}
return recycle;
}
需要注意的是 getInstance()方法的修饰符,它使用了 static 关键字,即它是一个静态方法,在类外可以直接通过类名来访问,而无须创建 RecycleBin 对象。

通过以上三步,我们完成了一个简单的单例模式的设计。这就是著名的懒汉式单例写法。

但是你以为这样就大功告成了吗?事情可没这么简单哦。

我们先写一个线程类 TestThread 类:

public class TestThread implements Runnable{@Override
br/>@Override
public void run() {
RecycleBin run = RecycleBin.getInstance();
System.out.println(Thread.currentThread().getName()+":" + run);
}
}
测试代码如下:

public static void main(String[] args) {
Thread t1 = new Thread(new TestThread());
Thread t2 = new Thread(new TestThread());
t1.start();
t2.start();
System.out.println("End");
}
运行结果:

技术图片
有一定几率出现创建两个不同结果的情况,意味着上面的单例存在线程安全隐患。

有时,我们得到的运行结果可能是相同的两个对象,实际上是被后面 执行的线程覆盖了,我们看到了一个假象,线程安全隐患依旧存在。那么,我们如何来 优化代码,使得懒汉式单例在线程环境下安全呢?来看下面的代码,给 getInstance()加 上 synchronized 关键字,使这个方法变成线程同步方法:

public synchronized static RecycleBin getInstance()
{
if (recycle == null)
{
recycle = new RecycleBin();
}
return recycle;
}
但是,用 synchronized 加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既兼顾线程安全又提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式:

public class RecycleBin {

private volatile static RecycleBin recycle = null;
private RecycleBin() {} //初始化窗口
public void restore() {} //还原文件
public void delete() {} //删除文件

public static RecycleBin getInstance()
{
if (recycle == null){
synchronized (RecycleBin.class){
if(recycle == null){
recycle = new RecycleBin();
//1.分配内存给这个对象
//2.初始化对象
//3.设置recycle指向刚分配的内存地址
}
}
}
return recycle;
}
当第一个线程调用 getInstance()方法时,第二个线程也可以调用 getInstance()。当第一 个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻 塞。此时,阻塞并不是基于整个 RecycleBin 类的阻塞,而是在 getInstance() 方法内部阻塞,只要逻辑不是太复杂,对于调用者而言感知不到。

但是,用到 synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。

此时的你有没有一个想法,如果我们可以不用 synchronized 关键字,是不是性能就能更好呢?难道就真的没有更好的方案吗?当然是有的。

可别急,在介绍性能更好的写法之前呢,我得给你先介绍介绍饿汉式写法。

public class RecycleBin {

private static final RecycleBin recycle = new RecycleBin();

private RecycleBin() {}

public static  RecycleBin getInstance()
{
    return recycle;
}

}
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。

优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。

缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能占着茅坑不拉屎

看了饿汉式的代码,是不是对你有点启发?如果我们可以兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题。是不是就能写出更优雅的代码了呢?

答案来了!我们可以从类初始化角度来考虑,看下面的代码,采用静态内部类的方式:

public class RecycleBin {

//默认使用RecycleBin的时候,会先初始化内部类
//如果没使用的话,内部类是不加载的
private RecycleBin() {}

//static是为了使单例的空间共享
//保证这个方法不会被重写,重载
public static final RecycleBin getInstance()
{
    //在返回结果以前,一定会先加载内部类
    return LazyHolder.recycle;
}

private static class LazyHolder{
    private static final RecycleBin recycle = new RecycleBin();
}

}
内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。

大家有没有发现,上面介绍的单例模式的构造方法除了加上 private 以外,没有做任何处理。如果我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会两个不同的实例。现在来看一段测试代码,以 RecycleBin 为例:

public class RecycleBinTest {
public static void main(String[] args) {
try {
Class> clazz = RecycleBin.class;
//通过反射,拿到私有构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//不给我也要拿
c.setAccessible(true);
//第一次初始化
Object o1 = c.newInstance();
//调用第二次构造方法,相当于new了两次
Object o2 = c.newInstance();

        System.out.println(o1 == o2);
    }catch (Exception e){
        e.printStackTrace();
    }
}

}
运行结果如下:
技术图片

显然,是创建了两个不同的实例。现在,我们在其构造方法中做一些限制,一旦出现多 次重复创建,则直接抛出异常。来看优化后的代码:

public class RecycleBin {

private RecycleBin() {
    if(LazyHolder.recycle != null){
        throw new RuntimeException("不允许创建多个实例");
    }
}

public static final RecycleBin getInstance()
{
    return LazyHolder.recycle;
}

private static class LazyHolder{
    private static final RecycleBin recycle = new RecycleBin();
}

}
至此,史上最牛 B 的单例写法便大功告成。

———— e n d ————
快年底了,师长为大家准备了三份面试宝典:
《java面试宝典5.0》
《350道Java面试题:整理自100+公司》
《资深java面试宝典-视频版》
分别适用于初中级,中高级,以及资深级工程师的面试复习。
内容包含java基础、javaweb、各个性能优化、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构、限流熔断降级等等。
技术图片
获取方式:点“在看”,V信关注师长的小号:编程最前线并回复 面试 领取,更多精彩陆续奉上。

一、初中级《java面试宝典5.0》,对标8-13K
技术图片
二、中高级《350道Java面试题:整理自100+公司》,对标12-20K
技术图片
三、资深《java面试突击-视频版》,对标20K+
技术图片

点在看好不好,喵~

【原创】从windows回收站谈单例

标签:volatil   href   ref   饿汉   准备   也会   构造方法   name   带来   

原文地址:https://blog.51cto.com/15009303/2552817


评论


亲,登录后才可以留言!