Java Concurrency in Practice 读书笔记 第二章

第二章  线程安全

2.1  什么是线程安全

定义1:无状态的对象一定是现成安全的。

如上所示,无状态是指:状态的瞬间转移全部蕴涵于Local变量中(只存在于本线程的堆栈中),且只能由当前线程来获取。
因此,如果只是Req<—–>Resp方式的话,Servlet一般都是线程安全的。但是如果要恢复/记录状态的话,就有可能导致线程安全问题。

2.2  原子性

定义2:对有状态类,如含类变量(Field变量)而言,如果不加控制的话,一般不是线程安全的。

例如下面代码不是线程安全的:

上述代码的问题并不在于使用了Field变量,而是使用了Filed变量却不加同步控制。因为++count不是原子操作,这会导致在某些杯具时间条件下产生的竞争条件

这一类竞争条件叫做check-then-act:例如检查某个变量的数值,然后准备对其进行修改,然而在你检查好之后、修改之前,这个数值却发生了变化,这就可能导致很多问题。

再来看一种lazy-load导致的竞争条件,这是基本所有人都会写错的单件类,因为它不是线程安全的:

如上所示,如果线程A在new对象的过程中,尚未赋值给instance之前,B来检查instance。B发现instance是null,也初始化了对象。然后单件类的功能就废了。

对于读-写-读的竞争条件,只要将读取i、自增i都做成原子操作就可以解决线程安全隐患了。

Java提供了AtomLong,即原子封装的Long来解决上述问题,如下述代码所示:

使用原子操作对象,是一种很好的线程安全解决办法。但是对包含两个及以上导致状态转移变量情况来说并不成立。

2.3  锁

如果同时有两个变量影响状态的变化,则不一定总是正确的,例如对于一个Cache因式分解的例子:

尽管lastNumber和lastFactors分别都是原子操作,但无法保证在一个原子操作内同时更新这两个数值。因此仍然不是线程安全的。

Java提供了synchronized关键字来完成同步工作。

在synchronized中包含的代码在同一时间只能被一个线程执行。

这种锁机制是可重入的:如果某线程尝试获取已经获得的锁,则它将直接获得,而非等待释放。

如果没有重入,下面这种代码将死锁:

如果同一线程中调用了子类的someThing,就会死锁。

2.4  用锁守护状态

如果使用synchronized同步变量,则必须在使用该变量的每一处添加synchronized。常见的错误认为:只需要在写的时候同步即可。

另一条规则:如果某一状态改变由多个变量共同作用,则为了同步它们,必须使用同一个锁。即将整体的状态改变放在同一个原子操作内。

此时,函数方式的synchronized能更好地解决这个问题:对于函数式synchronized来说,它使用的锁就是这个Object,因此所有同一对象内的synchronized函数使用同一个锁

然而,并非把所有函数都声明为synchronized就可以解决线程同步问题。

2.5  活跃度和性能

有的时候,往往要在性能和安全性之间选择做出平衡,如果对service全部synchronized,并发性会非常差尽管它是线程安全的,主要通过缩小synchronized的方法在保证线程安全的基础上提升并发性能,如下:

在上面的代码中,lastNumber、lastFactors、hints、cacheHits共同导致状态的变化,因此使用同一个锁:this(该对象)将他们保护起来。这样例如在修改lastNumber的时候,其他线程也不可能接触到其余变量。

保证线程安全的覆盖标准是:对于会导致状态变化的变量,在读、写他们的地方都要加上保护。

Leave a Reply

Your email address will not be published.