最近在学习《java并发编程实战》一书,为了有一些输出,并且记录自己的学习,整理一下自己的学习笔记,同时分享给大家。
1. 什么是线程安全
最核心的概念是正确性,正确性的定义是:某个类的行为和和其规范完全一致。
当多个线程访问某个类时,这个类始终能表现正确的行为,那么可以认为这个类是线程安全的。
注
:大多数servlet是无状态的,既不包含任何域,也不包含任何对其他类中域的引用。无状态对象一定是线程安全的。
2.原子性
假如希望增加“命中计数器”(hit counter)来统计处理的请求量,直观的方式是增加计数的域,每次请求时,域+1
//域
private long count = 0;
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
...
count++;
...
}
单线程下可以顺利执行,但是这个类可能会丢失一些更新操作,count++ 非原子性,实际上,它包含了三个操作:读取count的值,值+1,计算结果写入count。“读取-修改-写入”的操作序列
计数来说有时候缺失少量可以接受,但是如果是生成序列或者唯一标识的值,将会导致严重的数据完整性正确性的问题
并发编程中,由于不恰当的执行时序导致的执行结果不正确是一种非常重要的情况,它有一个很正式的名字:竞态条件(Race Condition)
3.竞态条件
竞态条件的本质:基于一种可能失效的观察结果 而 做出的判断或者执行某个计算。
这种类型的竞态条件称为“先检查后执行”,例如:某个时刻观察的结果为true,根据观察结果执行动作-创建文件X,事实上在观察和创建文件过程之间,这个观察结果发生了改变-可能无效(另一个线程已经创建了文件X),这就导致严重的后果(文件被覆盖,异常,数据不完整等)。
4.原子操作(复合操作)
为了确定安全性,“读取-修改-写入”、“先检查后执行”等操作必须是原子性的(复合操作)。
public final AtomicLong count = new AtomicLong();
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
count.incrementAndGet();
// ...
}
5.加锁机制
上述是通过AtomicLong以线程安全的方式管理计数的操作,是不是可以通过AtomicReference来管理最近执行的缓存状态呢?
private AtomicReference<BigInteger> lastNum = new AtomicReference<>();
private AtomicReference<BigInteger[]> lastFactories = new AtomicReference<>();
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
BigInteger i = process(servletRequest); //操作请求
if (i.equals(lastNum.get())){
lastFactories; //结果
}else {
BigInteger[] newFac = factor(i);
//更新
lastNum.set(i);
lastFactories.set(newFac);
}
原子引用本身是安全的,但是存在竞态条件。set无法同时更新2个值,当线程A获取2个值的过程中,如果线程B去修改了它们。
注
:要保持状态的一致性,就必须在单个的原子操作中 更新所有的状态变量。