单例模式

一次开发引起的思考

上篇博客中提到,可以通过自定TomcatConnectorCustomizer 接口中的方法实现SpringBoot优雅关闭时等待http请求的处理;可以通过钩子函数或者程序监听器实现程序关闭前的收尾处理;可以通过DisposableBean类在程序关闭前处理某些数据...这些看似不同的处理方法都存在一个相同点----对线程处理,tomcat最终要将connect使用的线程池关闭,而其它方法也不外乎如此。因此,找到这个共同点之后,对于我们即将要的线程池管理设计是十分有用的

前言

无论是在设计线程池管理类或是之后设计的缓存处理,通常都会这样一个逻辑:不论线程使用多少次,方法调用多少次,对象始终都得是一个,否则无法管理。打个比方,我们砍一棵树,这棵树长出多少树干树叶我们不必理会,最后只要将树砍到,那么就达到了我们的目的。那么如何确保只有一个对象,如何解决因一个对象引发的问题,就是本篇博客的内容。

单例模式

如何实现一个类只有一个对象?思路很简单:创建一个类,将该类的构造器私有化,同时new一个自己的对象,提供获取该对象的方法。这其实就是单例模式的基调,在单例模式中,已经有两种方式为我们实现这个功能

懒汉模式

public class Single{
    private Single() {}
    private static Single single = null;
    public static getSingle() {
        if (single == null) {
            single = new Single();
        }
        return single;
    }
}

当构造器被私有化之后,除了该类的内部成员可以访问之外其余皆不可访问。当getsingle方法被调用时,首先去判断single引用的对象是否存在,如果不存在则使其引用一个对象,否则返回其引用对象。其执行流程如下图所示
在这里插入图片描述

类在初始化时,并不会创建一个新的对象,只有当getSingle方法被调用了,这个对象才被创建出来,因此这种方法其实是节省资源的。但由于我们的设计类是存在于一个多线程环境下的,使用懒汉模式会存在线程安全问题。

安全问题

由于该类对象的判断条件是对象是否存在,因此当多个线程同时访问getSingle方法时,可能会出现这个问题:A线程执行getSingle方法进入if判断,此时判断结果为true,由于时间片不够,此时B线程也进入了if判断,由于此时并没有new一个对象,因此判断结果仍为true。这就会导致实际上new了两个对象,这是不能接受的。

如何解决这个问题,那么就要提一下锁

在Linux编程中,多进程、多线程的情况很常见,由于内核时间调度我们无法知晓,因此对于进程之间的执行顺序可以通过sleep函数去控制,某个进程sleep了,那自然它就被挂起,时间片分给了其它进程。线程之间的并发性更高,同时共享同一个地址空间,因此线程之间的资源竞争十分激烈,为了解决这个问题,因此采用了锁的方式。

线程访问资源,需要拿到锁,而锁只有一把。某个线程拿到锁之后,其它线程必须等待其将锁返回,再进行竞争。如下图

在这里插入图片描述

如何解决懒汉模式的线程安全问题,关键在于如何去实现锁

synchronized

synchronized关键字是java提供的同步锁,它可以将某些资源锁住,这些资源可以是类,可以是方法,也可以是对象。为了解决懒汉模式中的线程安全模式,可以将getSingle方法锁住,如

public class Single{
    private Single() {}
    private static Single single = null;
    public synchronized static getSingle() {
        if (single == null) {
            single = new Single();
        }
        return single;
    }
}

某个线程拿到getSingle的锁时即可以去执行接下来的代码段,而其它线程会被阻塞。不仅如此,我们也可以对该类上锁,此时该类中所有的资源都会被锁住

public class Single{
    private Single() {}
    private static Single single = null;
    public static getSingle() {
        if (single == null) {
            synchronized(Single.class) {
               if (Single == null)
                   single = new Single();
            }         
        }
        return single;
    }
}

当然,锁的处理应该要小心,毕竟在一个大量线程并发的瞬间,只有一个线程可以有效执行,而其它线程只能阻塞,这十分浪费内存,更不用说可能会导致死锁的情况发生。那么还有没有其它方式?

饿汉模式

既然懒汉模式的if判断语句会引起如此一连串的后果,那么饿汉模式只需要付出一点就能将这些问题完美解决。JVM虚拟机在加载类时,就让它把我们需要的对象new出来,用JVM线程来确保线程的安全性。

public class Single{
    private Single() {}
    private static Single single = new Single();
    public static getSingle() {
        return single;
    }
}

静态对象或者方法只在类的加载时初始化一次,此后不再执行。这样就无论是几个线程进来,它们也只能对该对象操作。

servlet线程安全

以上部分仅是对单个对象处理,而实际情况却没这么简单。每个对象都有属性,当多个线程对同一对象操作get,set方法时,也必然存在着安全问题。举个简单的例子,当某个线程对该对象的x属性进行set null操作,而另一线程进行get时就会出现NullPointException了。最好的例子就是SpringBoot Controller层使用的单例模式,同时20个http请求都是POST方法,那么如何去处理?

img

网上搜索了一下,servlet的调用过成大致如下

servlet调用过程

1.客户端发送请求给Tomcat,Tomcat调用Connecter组件接收

2.Connecter将其封装成一个HttpRequest对象,然后对请求进行处理,。

3.服务器找到已注册的servlet名称,同时找到我们全限定类名

4.找到全限定类名后,只创建一次servlet对象,之后使用的都是该实例

5.找到HttpServlet类

6.通过request对象取得客户端传过来的数据,对数据进行处理后通过response对象将处理结果写回客户端。

可以很明显的看到,servlet实例只创建一次,之后便被共享。那么多个线程对该资源的访问就肯定会造成安全问题了。归根结底,线程安全问题都是由共享资源引起的,如果能够像Linux中使用fork函数创建子进程中的写时复制(虽然这是进程的),那是否就可以解决这个问题了呢?

看到tomcat中对于servlet处理中提到一点:在JVM内存模型中,方法中的临时变量都放在栈上,而每个线程之间的栈是独立的,这样就可以保证线程安全。尽量不要在多线程的环境下使用实例变量。也就是说,servlet中传递的对象,实际也只是无状态变量的。

就像是SpringBoot中的Controller层是单例模式,如果其中存在有状态变量的话,就会出现线程安全问题,因此我们的Po,Vo层都是无状态类,此时其中的变量都是存在栈上的,因此不论多少线程,修改的都是自己私有的变量,不会出现问题。

结尾

如真的要在多线程下使用单例,并且是有状态的单例,除了加锁以外(十分浪费资源),还有没有什么其它方法保证线程安全问题?

参考

设计模式之单例模式
servlet线程安全

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值