第二章 线程安全性
value++
看上去是单个操作,但它包含了三个独立的操作
- 读取value
- 将value加一
- 将结果写入value(赋值)
线程的风险
-
安全性
问题(多个线程共享同一个变量,如果不处理,可能会出现一些不可预知的错误) -
活跃性
问题(单线程也有问题,关注的目标:某件正确的事情最终会发生
。比如可能会出现无意造成的无线循环
等等问题) -
性能
问题(线程调度、上下文切换等等)
线程安全类
最核心的概念就是正确性(某个类的行为和其规范完全一致)
当多个线程访问某个类时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的
无状态(无状态的对象一定线程安全)
- 不包含任何域
- 也不包含任何对其他类中域的引用
竞态条件(Race Condition)
在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件
例子:先检查后执行
原子方式(不可分割)
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B。那么A和B对彼此来说都是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作
- 要保持状态的一致性,就需要在单个原子操作中更新
所有
相关的状态变量 - 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁。在这种情况下,我们称状态变量是由这个锁保护的
- 每个共享的和可变的变量都应该只由一个锁来保护
第三章 对象的共享
主要介绍如何共享和发布对象,从而使它们能够安全地由多个线程同时访问
synchronized
- 实现原子性和确定
临界区
- 内存可见性
3.1可见性
static
不保证可见性,多线程时可能会出问题
指令重排序
在没有同步的情况,编译器、处理器以及运行时
等都可能对操作的执行顺序进行一些意想不到的调整。
非原子性的64位操作
- 32位操作系统上对64位变量读写的非原子性,高32位和低32位分开操作
- 64位操作系统上读写64位数据是原始操作。
加锁与可见性
加锁的含义不仅仅局限于互斥
行为,还包括内存可见性
。为了确保所有线程都能看到共享变量的最新值
,所有执行读操作
或者写操作
的线程必须在·同一个锁上同步·
当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作。
volatile变量
一种稍弱
的同步机制,用来确保将变量的更新操作通知到其他线程
- 编译器和运行时
不会
将该变量上的操作和其他内存操作一起重排序
- volatile变量不会被
缓存
在寄存器
或者对其他处理器不可见
(同一个处理就是可见的?)的地方
volatile不保证原子性
:比如i++
3.2 发布和逸出
“发布publish”
就是对象能够在当前作用域之外的代码中使用
逸出
:当某个不应该发布的对象被发布时
常见的不小心逸出的场景
- 将引用保存到一个共有的静态变量中。(如果这个变量是个集合或数组,那么里面的元素
全都
逸出了) - 被类一起发布出去
- 内部类发布(因为内部类中会有对外部类的引用)
安全的对象构造过程
不要在构造过程
中使用this逸出(因为此时的对象可能还未完成构造,状态时不可预测的)
- 不要在构造函数中启动一个线程
- 也不要在构造函数中注册监听器等等
因为如果是内部(局部)类都会逸出未构造完成的this
。
3.3 线程封闭
不共享数据。仅在单线程内访问数据,不需要同步
ThreadLocal
可以共享变量,减少方法参数传递
。但也要小心,因为会降低代码可重用性、增加隐含的耦合性
。
3.4 不变性
不可变对象一定是线程安全的
很多定义都是从代码上看的,但自己在使用中:很多都是逻辑上的不可变
(即没有去改动)。
3.4.1 final域
final修饰的变量是线程安全的。
好的编程习惯:除非某个域是可变
的,否则应将其声明为final域
。
3.4.2 使用volatile来发布不可变对象
不可变对象也能解决竞争条件问题
比如一些操作因为因为线检查精后处理得问题,导致竞争条件出现
现在我们可以我们可以通过final和volatile结合,将操作封装到一个不变的对象中(保证对应关系不会因为并发问题乱掉)也就是一致性
,volatile保证数据的实时性。
3.5 安全发布
只要没有使用同步来确保对象对其他线程可见,那么就被称为'未被正确发布'
3.5.1 不正确的发布
比如
public Holder h;
public void init(){
h = new Holder();
}
这段代码的问题在于:
只要没有同步,那么其他线程不一定能看到构造函数结束后
的状态(可能构造了一般,其他线程就调用了该对象)构造函数执行过程中:会调用Object的构造函数,属性初始值默认,然后赋值
3.5.2 不可变对象和初始化安全性
- 即使某个对象的
引用对其
他线程是可见的
,但不意味
着对象状态
对于使用该对象的线程一定是可见
的 - java对final域做了处理
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象
.
网友评论