线程安全策略

保证线程安全的策略大体可以分为以下几个:

  • 不可变对象:对象只能读,不能修改,从根源上消除多线程的不安全性。
  • 线程封闭:线程之间不共享变量。
  • 同步容器:使用同步关键字修饰关键方法,保证对象关键操作的线程安全。
  • 并发容器:针对同步容器进行优化。

不可变对象

大部分线程不安全的因素,是因为多个线程同时修改共享变量导致的冲突,所以,如果把对象设置为不可变的,就能够避免相当一部分麻烦。

final

final关键字修饰的对象的值是不能够再被修改的,对于基本数据类型以及String,使用final修饰,就意味着它们再不能被修改。

final修饰的类:不能够被继承,其中的方法也隐式地被final修饰;final修饰的方法:不能被子类重写;final修饰的对象:值不能被修改。

String类是不可变对象中的典范,它只要被初始化之后,就不能再被改变了,其中主要用到了以下的手法:

  1. 用于存放值的数组被设置为final;
  2. 和外界相交互的的方法中,均使用深度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类都是采用了这种思路。一旦请求执行的线程量较大,这种方式将严重影响执行效率,同时同步容器仅把单一的操作调整成为了线程安全的,多个操作之间的组合依旧会存在线程安全问题。

并发容器

针对同步容器的效率以及安全性问题,并发容器应运而生。

待续~

posted @ 2021-02-20 16:29  sunnysgw  阅读(83)  评论(0)    收藏  举报