线程安全策略
保证线程安全的策略大体可以分为以下几个:
- 不可变对象:对象只能读,不能修改,从根源上消除多线程的不安全性。
- 线程封闭:线程之间不共享变量。
- 同步容器:使用同步关键字修饰关键方法,保证对象关键操作的线程安全。
- 并发容器:针对同步容器进行优化。
不可变对象
大部分线程不安全的因素,是因为多个线程同时修改共享变量导致的冲突,所以,如果把对象设置为不可变的,就能够避免相当一部分麻烦。
final
final关键字修饰的对象的值是不能够再被修改的,对于基本数据类型以及String,使用final修饰,就意味着它们再不能被修改。
final修饰的类:不能够被继承,其中的方法也隐式地被final修饰;final修饰的方法:不能被子类重写;final修饰的对象:值不能被修改。
String类是不可变对象中的典范,它只要被初始化之后,就不能再被改变了,其中主要用到了以下的手法:
- 用于存放值的数组被设置为final;
- 和外界相交互的的方法中,均使用深度copy,防止value被修改。
封装对象
使用final修饰的对象仅仅能够保证对象在堆中的地址不会被修改,即该对象不会再指向另一个地址,但是仍然能够在堆中修改对象的值。所以,如果想要把对象调整为彻底不能被修改,需要进行特定的封装。针对List、Set以及Map,java都提供了对应的不可变类,其中修改对象的操作被禁止。
线程封闭
如果多个线程之间不需要共享变量,那么干脆将不同线程中的变量分开存放。
堆栈封闭
直接将对象定义在方法体内,是实现线程封闭的最简单也是最有效的方式,每个线程都在自己的工作内存中处理对象,不会干扰到其他的线程,当跳出对象的作用域,那么存放在工作内存中的对象将会被清空。
ThreadLocal
维护了一个Map对象,其中key即子线程的id,value即要在不同线程中要存放的值。
接触过的一个例子就是在MyBatis的PageHelper插件(基于代理以及ThreadLocal实现)中,使用ThreadLocal存放要分页的参数,包括pageSize以及pageNum等,之后在同一个线程中执行sql语句的时候,会依据ThreadLocal中存放的分页参数对要执行的sql语句进行调整,达到分页的目的。因为要执行的sql语句以及对应的分页参数之间不是强制绑定的,所以有些情况下会出现不可预知的问题。如果设置分页参数之后,还没来得及执行sql语句,子线程报错,终止执行,此时分页参数并不会被主动清空,这意味着该线程再次执行sql语句的时候,将会使用之前定义好的分页参数进行分页,反应在具体项目中就是有些sql语句被莫名其妙地加上了limit,如果这些语句后面有;,那么会直接报错。
针对以上问题,在使用ThreadLocal存放数据之后,如果数据不再被使用,要及时清理,避免导致不必要的麻烦。所以,可以使用try finally块包裹要进行分页的语句,在finally中将分页数据清空,避免对其他sql语句造成干扰。
同步容器
同步容器中,使用同步关键字修饰会导致线程安全问题的代码块,将之限制为串行,java.util.Collections中的synchronizedxx类都是采用了这种思路。一旦请求执行的线程量较大,这种方式将严重影响执行效率,同时同步容器仅把单一的操作调整成为了线程安全的,多个操作之间的组合依旧会存在线程安全问题。
并发容器
针对同步容器的效率以及安全性问题,并发容器应运而生。
待续~

浙公网安备 33010602011771号