浅谈线程安全与否之重要性
很久没有写过博客了,这算是一年来第一篇公开的博客来着
开宗明义,先上一篇我觉得很棒的博客
Java中的多线程你只要看这一篇就够了
先介绍为什么会出现线程不安全的情况:计算机在执行多线程任务的时候,如果所能调度的只有单核,那么实际上的物理执行为:线程A执行部分过后暂停,然后线程B继续执行。这时,如果程序有全局变量或者静态变量的存在,同时两个线程内有对于全局或者静态变量的操作,那么很有可能导致的情况就就是两个线程在执行的时候,发生在对于同一个全局变量进行操作之后,主线程的内存被更新,而同时,子线程内存未更新而就开始进行操作操作以至于拿到不同的结果。例如如下代码:
count.java
1 | public class Count { |
ThreadTest.java
1 | public class ThreadTest { |
运行之后,可以发现,结果是
1 | Thread-0-165 |
可以看到,每个线程输出的数字都不太一样,在最后输出了550,也就是十个线程递增的结果。
而不是我们所期望的每个线程都输出55
那么为什么会造成这种情况呢?
答案是因为,CPU在执行多线程任务的时候,每个线程并非可以独立占用一个CPU,而是将一段代码分成几部分,然后各个线程的部分代码交替执行。
例如这里的子线程操作,一共如下几个步骤:
- 读取主线程中count对象到子线程内存中。
- 子线程操作引擎执行操作代码
- 将计算结果从子线程内存中刷新到主线程
但是在多线程的情况下,可能出现的情况为
线程1-1,线程2-1,线程1-2,线程1-3,线程2-3 或者其他的排序,因为CPU具体在执行代码的时候,会对于操作进行重排序,而排序情况是不确定的。
对此就引申出多线程几个重要的概念:
原子性:是指一个操作是不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值为1,线程B给他赋值为-1。那么不管这两个线程以何种方式。何种步调工作,i的值要么是1,要么是-1.线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。
可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行来说,可见性问题是不存在的。
有序性:在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。
而线程不安全,就是违背了三个特性中的某一个特性而导致的输出非预期输出或其他问题。同时,解决线程的安全性与否,亦是从上面三个性质入手开始解决。例如之前的例子,便是由于count对象的num变量是成员变量,同时count对象是主线程内共享的,因此导致的问题就是上一个线程执行完以后,下一个线程拿到了count对象,并且读到了其成员变量num被修改过的值,所以导致了数字是累加而不是每个线程独立运算结果。
解决的方法有两种,第一个是将Count类里面的num作为局部变量使用,第二个是将count对象放在run方法里面,也就是将成员变量和共享的全局变量,改为局部变量。
我们将这个问题引申,当我们在写Java Web的代码时,由于Servlet接口是单实例,多线程的运行方式,也就是每个请求进来以后,单独开一个线程对于请求进行处理。但是由于上面所说,对于每个请求都是单实例但是多线程的方式,如果在Servlet中有了一个共享的成员变量或者全局变量,这样就会导致如果有线程在调用实例之后,对成员变量进行修改,那么另一个线程在调用实例时,并不知道成员变量被修改,而是遵循原来的逻辑直接输出,这样就会导致同样的输入可能得到不同的结果。
对此,我们有了第一种解决方法,因为被修改之后,下一个线程不一定能及时的拿到主线程内存的更新状态,因此我们需要做的便是,保持成员变量或全局变量的更新。这里引入volatile关键字。
定义:volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
1 | class Test { |
one方法和two方法还会并发的去执行,但是加上volatile可以将共享变量i和j的改变直接响应到主内存中,这样保证了主内存中i和j的值一致性,然而在执行two方法时,在two方法获取到i的值和获取到j的值中间的这段时间,one方法也许被执行了好多次,导致j的值会大于i的值。所以volatile可以保证内存可见性,不能保证并发有序性。
第二种解决方法便是synchronized关键字
1 | public class TraditionalThreadSynchronized { |
- 使用synchronized将需要互斥的代码包含起来,并上一把锁。
- 将synchronized加在需要互斥的方法上。
对于Servlet中的线程安全问题
1,变量的线程安全:这里的变量指字段和共享数据(如表单参数值)。
a,将 参数变量 本地化。多线程并不共享局部变量.所以我们要尽可能的在servlet中使用局部变量。
例如:String user = “”;
user = request.getParameter(“user”);
b,使用同步块Synchronized,防止可能异步调用的代码块。这意味着线程需要排队处理。
在使用同板块的时候要尽可能的缩小同步代码的范围,不要直接在sevice方法和响应方法上使用同步,这样会严重影响性能。
2,属性的线程安全:ServletContext,HttpSession,ServletRequest对象中属性
ServletContext:(线程是不安全的)
ServletContext是可以多线程同时读/写属性的,线程是不安全的。要对属性的读写进行同步处理或者进行深度Clone()。
所以在Servlet上下文中尽可能少量保存会被修改(写)的数据,可以采取其他方式在多个Servlet中共享,比方我们可以使用单例模式来处理共享数据。
HttpSession:(线程是不安全的)
HttpSession对象在用户会话期间存在,只能在处理属于同一个Session的请求的线程中被访问,因此Session对象的属性访问理论上是线程安全的。
当用户打开多个同属于一个进程的浏览器窗口,在这些窗口的访问属于同一个Session,会出现多次请求,需要多个工作线程来处理请求,可能造成同时多线程读写属性。
这时我们需要对属性的读写进行同步处理:使用同步块Synchronized和使用读/写器来解决。
ServletRequest:(线程是安全的)
对于每一个请求,由一个工作线程来执行,都会创建有一个新的ServletRequest对象,所以ServletRequest对象只能在一个线程中被访问。ServletRequest是线程安全的。
注意:ServletRequest对象在service方法的范围内是有效的,不要试图在service方法结束后仍然保存请求对象的引用。
3,使用同步的集合类:
使用Vector代替ArrayList,使用Hashtable代替HashMap。
4,不要在Servlet中创建自己的线程来完成某个功能。
Servlet本身就是多线程的,在Servlet中再创建线程,将导致执行情况复杂化,出现多线程安全问题。
5,在多个servlet中对外部对象(比方文件)进行修改操作一定要加锁,做到互斥的访问。
PS:SingleThreadModel接口亦可以 只不过已经被弃用
Spring中的线程安全问题
- 对操作共享变量的所用方法进行同步控制;
- 同步共享变量,例如Collections.synchronizedMap()可以同步共享的Map。
- 使用同步对象,例如ConcurrentMap、AtomicInteger等对象都是线程安全的,使用AtomicInteger可以统计系统的并发量。
内存泄露
- 多关注集合类,比如HashMap,ArrayList等,这些对象经常会发生内存泄露。比如当它们被声明为静态对象时,它们的生命周期会跟应用程序的生命周期一样长,很容易造成内存不足。
- 多关注事件监听(listeners)和回调(callbacks),比如注册了一个listener,当它不再被使用的时候,忘了注销该listener,可能就会产生内存泄露。