第二章:线程安全
1.什么是线程安全
当多个线程并发的访问一个类,如果不用考虑多个线程运行时的调度执行顺序,且不需要做额外的同步及代码调用时候的限制,这个类的结果依然是正确的,则可以称之为线程安全.
无状态的类是线程安全的,无状态指不包含域也没有引用其他类的域.如下面这个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
2.原子性
常见的竞争条件1:检查再运行(check_then_act).检查再运行指使用潜在的过期值来做决策或者执行计算.如下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
常见的竞争条件2:读-改-写.如count++ 这种非原子的操作,其实包含三个原子操作
1 2 3 |
|
什么是原子操作:
假设有操作A,在其他线程来看,执行操作A的线程执行时,要么操作A执行完成,要么一点都没有执行.则称之为原子操作.
如果前面的例子是原子的,则不会出现检查再运行和读-改-写的竞争条件.
为例保证线程安全,操作必须原子的执行.
比如count++,我们可以使用AtomicInteger:
1
|
|
3.锁
java提供了强制原子性的内置锁机制:synchronized块.一个synchronized块包含两部分:1.锁对象的引用;2.锁对象保护的代码块;
1 2 3 |
|
每个java对象都代表一个互斥锁(称之为内部锁或者监视器锁),因此每个java对象都可以扮演lock的角色.对于方法级别的synchronized,获取的是方法所在对象的锁.对于静态方法,从Class对象上获取锁.
当一个线程请求另外一个线程持有的锁时候,请求线程将会被阻塞.但是,内部锁是可以重入(Reentrancy)的,即线程请求它自己占有的锁的时候,是会成功的.这表示锁的请求是基于每个线程(per-thread)的,而不是基于每个调用(per-invocation)的.
4.用锁来保护状态
对于可以被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,则称这个变量是由这个锁保护的.
并不是所有数据都需要锁的保护,只有哪些被多个线程访问的可变数据.
如果类的不变约束涉及到多个变量,则需要用同一个锁来保护这多个变量.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
5.活跃度和性能
为了达到安全的目的,我们完全可以将方法设置为synchronized,但是这样带来的问题是:我们期房方法能提供并发访问,但是为了安全,实际上变成的串行访问.
因此在使用synchronized的时候需要考虑安全(不能妥协),简单性和性能.
比如网络或者IO这种耗时的操作器件,不应该占用锁.