一次开发引起的思考
上篇博客中提到,可以通过自定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方法,那么如何去处理?
网上搜索了一下,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层都是无状态类,此时其中的变量都是存在栈上的,因此不论多少线程,修改的都是自己私有的变量,不会出现问题。
结尾
如真的要在多线程下使用单例,并且是有状态的单例,除了加锁以外(十分浪费资源),还有没有什么其它方法保证线程安全问题?