1. 什么是线程安全性问题
在多线程同时访问同一段代码的情况下,无论线程怎么切换,怎么交互都不会出现脏读[1],丢失更新[2],代码都能正常运行,这样就称之为线程安全了。
2.出现线程安全问题的原因
有线程安全,就存在线程不安全,那么我们接下来通过线程不安全的例子,来找到线程不安全的诱因。
线程不安全的例子如下:
public class UnsafetyDemo {
public static void main(String[] args) {
ShareResourse3 resourse = new ShareResourse3();
new Thread(resourse).start();
new Thread(resourse).start();
new Thread(resourse).start();
}
}
class ShareResourse3 implements Runnable {
private int num = 0;
public int getNum(){
return ++num;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->" + getNum());
}
}
运行结果:
Thread-1-->1
Thread-2-->2
Thread-0-->1
上面代码运行不正常,出现了线程不安全,如果线程安全运行结果最后得到的num应该为2。
num++存在原子性问题(请参考上一篇文章《线程并发--原子变量解决自增自减原子性问题》),怎么验证呢?我们可以通过javap -verbose ShareResourse3.class命令反汇编查看ShareResourse3这个类的字节码,截取其中我们关心的getNum方法中的反汇编命令:
image.png image.png分析如下:
①Thread-0线程抢到了时间片,线程开始执行,此时Thread-0获取到的num为0,执行到指令5的时候时间片用完,Thread-0运行暂停在了指令5,num还是为0.
②Thread-1线程抢到了时间片,开始执行代码,此时Thread-1获取到的num还是为0因为上一个线程没有改变num的值,Thread-1执行比较快,Thread-1执行完了并打印了之后时间片才用完,此时就在控制台上打印出了“Thread-1-->1”结果,而且num为1
③Thread-1线程时间片用完了,被Thread-2线程抢到了时间片Thread-2开始执行完毕并打印“Thread-2-->2”结果。此时num为2
④最后Thread-1和Thread-2都执行完了,没有线程抢时间片了,Thread-0重新抢到了时间片,从上次停的指令6开始运行,此时的num没有重新获取还是之前停下来的0,继续加1,为1,所以打印“Thread-0-->1”
通过上述例子,我们可以得到结论,导致线程不安全原因有以下三点:
多线程环境下
多个线程共享一个资源
对资源进行非原子性操作
3.servlet是线程安全吗?
答案:不安全的。我们用代码和结果来说明吧!
代码和结果如下:
public class ThreadServlet extends HttpServlet {
private int i = 0;
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(this + "--->" + Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}
运行结果:
image.png运行结果很显然,出现了线程不安全问题,我们一起来分析一下原理是什么?为什么servlet线程不安全?
servlet线程不安全原理:
我们可以看到运行结果来,从结果上分析,很显然用户使用浏览器访问服务器中的servlet时,每一个http请求都是一个线程,访问多次就是多个线程访问ThreadServlet中的实例资源i,但是当Tomcat接收到Client的HTTP请求时,Tomcat只会创建一个servlet,因为servlet是单例的,那么很显然现在就是在多线程的情况下,多线程访问同一个资源,并且进行着非原子性操作。所以出现线程不安全。
如图所示:
image.png4.解决Servlet线程问题有三种方式:
-
将非原子性代码放入到synchronized代码块中,但是synchronized代码块会大大降低性能。
-
servlet实现 SingleThreadModel 接口,规定该servlet对象只能被一个线程访问,就不存在线程不安全了,但是SingleThreadModel 接口已经被定义为过时接口了不推荐使用,因为它是通过创建多个servlet对象,servlet不再是单例,而是一个servlet对应一个线程的方式,这样创建很多的servlet对象,浪费资源。
运行结果如下:
image.png- 所以在servlet中尽量不使用实例变量,尽量不要共享内存资源就好了
【相关词汇】
[1]脏读:就是一个线程在修改还没有修改完,另外一个线程就将这个没有被修改完的数据读取了
[2]丢失更新:两个线程同时获取数据同时做修改,后面更新的线程会将前面的更新的线程更新的数据覆盖
网友评论