要编写线程安全的代码,其核心是对状态访问进行管理,特别是对共享的(Shared)、可变的(Mutable)状态进行管理。
对象的状态是指:储存在状态变量中的数据(指可影响任何外部可见行为的数据),还可能包括其他依赖对象的域。
当多个线程访问一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。三种方式修正这个错误:
① 将可变状态变量转换为不可变的变量
② 不在线程之间共享该状态变量
③ 在访问状态变量时使用同步
线程安全的程序并非一定是由线程安全类构成的,线程安全类中也可以包含非线程安全类。
???
2.1 什么是线程安全性
定义:当多个线程访问某个类时,该类始终表现出正确性,那么该类就是线程安全的。
在一个线程安全类中实现了必要的同步机制,因此客户端无需进一步采取措施。
无状态的对象一定是线程安全的
无状态:既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态只能存放于线程栈上的局部变量,并且只能有正在执行的线程访问。
2.2 原子性
++count是一个非原子性操作,它由三个状态“读取-修改-写入”构成,最终的状态由读取的之前的状态决定。
在并发过程中,由于不正确的执行顺序导致不正确的结果的发生的现象叫做:竞态条件
2.2.1 竞态条件
“先检查-后执行”:就是指检查过的结果在执行时已经失效,因为在检查过和执行之间的时间会有新事件发生。
2.2.2 延迟初始化(先检查后执行)
假设两个线程同时调用 getInstance(),会出现竞态条件。
2.2.3 符合操作
以上例子表明,要避免竞态条件,就必须在某个线程修改变量时,阻止这个操作,防止其他线程使用这个变量,从而确保该访问过程出现在修改前或者修改后,而不是修改中。
为了确保线程安全性,“先检查,后执行”(延迟初始化)和“读取-修改-删除”(count++)等操作必须是原子的。
通过AtomicLong 来代替 long类型的计数器,能够确保所有对计数器状态的访问状态都是原子性的。
在一个无状态的类中添加一个状态时,如果该状态是由线程安全对象管理的,那么该类任然是线程安全的。
要尽可能用线程安全对象。
2.3 加锁机制
仿照上例:在Servlet中添加更多的状态是否只需要添加更多的线程安全对象就足够了。
不正确:
尽管这些原子引用本身是线程安全的,但在此类中存在竞态机制,
2.3 加锁机制
仿照上例:在Servlet中添加更多的状态是否只需要添加更多的线程安全对象就足够了。
不正确:
尽管这些原子引用本身是线程安全的,但在此类中存在竞态机制,因此也不是线程安全的类。
当不变性条件下涉及多个变量时,各个变量间不是彼此独立的,某个变量的值会约束到其他变量的值。因此,当一个变量发生变化时,需要在一个原子操作中对其他变量进行操作。
set方法,get方法应保证在number和factor之间的原子性,get要原子内两者都get,set要原子内set。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
2.3.1 内置锁
Synchronized (lock){
//访问或修改由锁保护的共享状态
}
每个Java对象都可以做一个实现同步的锁,这些锁被称为内置锁或者监视器锁。线程在进入同步代码块时会自动获取锁,退出代码块时会自动释放锁。
java中的锁相当于一种互斥体,也就是只有一个线程能拥有这个锁。当A企图获取B持有的锁时,A必须阻塞知道B释放,若B不释放,A将一直阻塞下去。
虽然线程安全,但是性能太低,因为同一时刻只能有一个线程执行service