第十五章 原子变量与非阻塞同步机制
并发包里的工具(如信号量Semaphore
和ConcurrentLinkedQueue
)
比synchronized
性能更好,伸缩性更好.
原因是使用了原子变量与非阻塞同步机制
.
非阻塞的底层: CAS操作. (compare and swap)
没有锁,Lock free
,因此更接近完美.
因此没有活跃性问题.
15.1 锁的劣势
性能不行.
15.2 硬件对并发的支持
独占锁: 基于悲观假设,不互斥的话会出事.(悲观锁)
乐观方法: 特殊指令: 包括, Test And Set , Fetch and Increment, CAS, 条件存储.
15.2 CAS 比较并交换
CAS
包括三个操作数: 内存位置,旧值,新值.
//类似对应java中map
的replace
操作的三个操作数: key,旧值,新值.
CAS操作失败的线程不会挂,会获得失败信息.(同步非阻塞)
可以反复重试. (类似于poll
的轮询)
CAS的缺点:
需要调用者自己处理竞争问题.// 重试,回退,放弃.
CAS实现的非阻塞算法通常比用锁写复杂一些.
15.3 原子变量类
包括AtomicInteger
等等.提供各种原子操作,如CAS,自增等.
它和一样保证写后读,但还保证内容是原子更新的.(比如两块内存看似一起更新)
- tip:
原子变量类不适合作为容器的Key.
容器的Key一般要是Immutable对象.(如Integer,String)
15.4 具体案例
这一节用CAS实现了链表,栈,原子的域更新器.
需要看着源码学习.
15.4.4 ABA问题
CAS(V,A,B)需要判断V位置是否为A,然后替换为B.
但如果V位置发生变化: A->B->A.
这样其实和我们希望的条件已经不同了,本质上是变化了,只不过值没变,版本号变了.
实例来说,就是链表节点引用没变,引用的值已经变了.
解决方案:
使用AtomicStampedReference
(以及AtomicMarkableReference
)支持在两个变量上执行原子的条件更新.
AtomicStampedReference
[对象,引用]二元组,在引用上加上版本号.AtomicMarkableReference
[对象引用,布尔值]二元组,可标记节点为已删除的节点.
第十六章 java内存模型
16.1 概念
为了性能,会进行指令重排,预测执行等等.
内存模型: JMM
.
内存模型规定,优化(重排)时应该遵守哪些约束.
- 串行一致性:
如果约束够用,执行结果就会和串行一样.
16.1.1 平台的内存模型
平台给出内存模型,约束自己,向外界保证xx条件下会发生什么.
提供接口/指令: 如内存栅栏,提供协调存储的接口.
16.1.3 JAVA内存模型
- 程序顺序: 同一个线程里按源码顺序执行;
- 监视器锁: 解锁会在加锁之前;(A线程释放了,B线程才能获得锁)
- volatile: 写后读. (原子变量也是)
- 线程启动: Thread.start之后才会有run等其他操作发生.
- 中断规则: (1)A线程中断B线程;(2)B检测到中断. 保证(1)在(2)前面.
- 终结器: 构造函数在终结器之前执行完成.
- 传递性: 上述规则可以传递.
1是针对单一线程的,2-7都是针对多个线程之间的代码顺序.
口诀: 程锁原线中终传.
16.1.4 借助同步
也就是借助上述7个已有的约束设计代码,达到同步效果.
比如我们平时用的锁,就是利用用了第二条,监视器锁规则.
类库中提供的约束:
- 线程安全容器: 写后读.
CountDownLatch
: 倒数操作在await返回之前.Semaphore
: PV操作,V释放在P获得之前. (别人释放了,你才能获得)Future
: get返回之前,任务的代码会执行完.Executor
: 提交任务操作,将在执行任务操作之前.CyclicBarrier
: (1)线程A到达栅栏;(2)其他线程离开栅栏. (1)会在(2)前面.换句话说,大家伙会等线程A到了才离开.
…
16.2 发布
错误示例:
1 |
|
安全但是慢:
1 |
|
直接静态初始化:
1 |
|
占位符技术: 延迟静态初始化:
1 |
|
双检(DCL): 不推荐. 慢,繁琐.
1 |
|
初始化安全域:
对象的初始引用不会被重排序到构造函数之前.
final
:final
域能够安全发布,通过final
域可达的变量\容器的写入操作安全发布.
示例:
1 |
|
线程A创建的SafeStates对象,线程B能安全得访问getV方法.