并发编程-基础篇
前言
文档类型
- 此文档属于参考手册类文档,只供查阅,所以不会涉及详细的过程说明。
参考书籍
- Java并发编程的艺术(方鹏飞 等著)
- 实战Java高并发程序实际(葛一鸣 著)
java对象头
对象头的长度
当前对象类型 | 对象头长度 |
---|---|
数组 | 3字长 |
非数组 | 2字长 |
对象头的存储结构
长度 | 内容 | 说明 |
---|---|---|
1个字长(32bit/64bit) | Mark World | 存储对象的hashCode以及锁的信息 |
1个字长 | Class Metedata Address | 存储对象类型数据的指针 |
1个字长 | Array length | 数组长度(如果对象是数组) |
初始MarkWorld
锁状态 | 25bit | 4bit | 1bit偏向锁标志位 | 2bit锁标志位 |
---|---|---|---|---|
无锁 | hashCode | 对象分代年龄 | 0 | 01 |
随着锁标志位变化的MarkWorld
锁状态 | 30bit | 2bit锁标志位 |
---|---|---|
轻量级锁 | 指向线程栈中锁记录的指针 | 00 |
锁状态 | 30bit | 2bit锁标志位 |
---|---|---|
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
锁状态 | 30bit | 2bit锁标志位 |
---|---|---|
GC标记 | 空 | 11 |
锁状态 | 23bit | 2bit | 1bit偏向锁标志位 | 2bit锁标志位 |
---|---|---|---|---|
偏向锁 | 线程ID | Epoch | 1 | 01 |
volatile
- volatile保证内存的可见性,不能保证一致性
- 当线程访问到有volatile修饰的变量a时,会将当前线程栈中的a设为无效,将包含变量a的缓存行设置为无效,重新从内存中读取该变量的值
- 当线程需要修改变量a时,会将变量a的值刷新到主存中,保证其他变量可以看到该线程对这个变量的修改
- 有线程thread_a和线程thread_b访问同一个变量a时。假设thread_a先访问到变量a时,java规定,根据happenss-before原则,保证线程thread_a对变量a做的操作,一定能被后访问变量a的线程thread_b感知到,就是说线程thread_b能访问到的一定是最新的数据。
- volatile修饰的变量,禁止对volatile重新排序。加入内存屏障,确保语义的正确。
synchronize
- synchronize针对对象加锁,具体来说是对类,或者类的实例加锁。
- 锁的信息保存在对象头中的。
偏向锁
加锁
- 当线程访问同步块时:在对象头和栈帧中的锁记录里存储锁偏向的线程ID。
- 以后当该线程进入和退出同步块时,不需要进行CAS操作来加锁和解锁,只是需要简单测试一下对象头的Mark World里是否存储着当前线程的偏向锁。
- 如果测试成功,则代表当前线程已经获取到了锁。如果测试失败,测试一下MarkWord中偏向的标志是否设为1。
- 如果设置了偏向锁,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销
轻量级锁
加锁
- 线程执行同步块之前,会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord复制到锁记录中。这个记录的锁成为Displaced Mark Word。
- 然后线程尝试使用CAS将对象头的MarkWod替换为指向锁记录的指针。如果成功,当前线程获取锁。如果失败,表示其他线程竞争锁,当前线程尝试自旋来获取锁。
- 如果自旋失败,则将对象头中的MarkWord中的锁改为重量级锁,线程进入到阻塞队列中。
释放锁
-
当获得锁的进程退出同步代码块时,用CAS操作比较:
发现对象头的MarkWord被改变(有锁竞争时,会将锁标志位标记为重量级锁),则释放锁并从阻塞队列中唤醒等待的线程。
发现对象头中的MarkWord没有改变,则恢复MarkWord为初始状态(其他线程可以获取了)。
重量级锁
经过轻量级锁变化得到的重量级锁。转化为重量级锁的前提是:多个线程存在竞争锁。
锁的比较
锁类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁解锁不需要额外消耗,和执行非同步方法差不多 | 如果存在锁竞争,会带来额外锁撤销的消耗 | 只有一个线程访问同步块 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 得不到锁竞争的线程,自旋消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程不使用自旋,不消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
CAS(Compare And Swap)
-
原子性
-
CAS是底层提供的一个调用。
-
CAS(expected_a,altered_a)
-
current_a:函数调用时,从内存中读取到的a的值
-
expected_a:调用这个函数时,你认为当前的a的值
-
altered_a:你希望修改后的a的值
如果内存中的a值和你认为的a值一致,即在当前线程对a变量进行修改时,没有其他线程对a进行过修改,当前修改操作成功,返回成功。
IF current_a == expected_a:
current_a = altered_a
RETURN TRUE
如果值不一致,即当前线程对a变量进行修改时,有其他线程对a变量进行修改过,当前修改操作终止,返回失败。
ELSE
RETURN FALSE
缺点
ABA问题
两个线程进行同步操作,线程1获取到共享变量V的值为A,然后此时线程2对共享变量V进行操作将它改为B,然后又改为了A,此时线程1进行CAS操作发现V的值还是A,认为这段时间没有线程对V进行操作,然后执行对V的操作。这听上去没有什么问题,然而实际应用中会带来意想不到的问题。
发生原因
这个问题存在的根本原因是CAS只对值进行判断,会丢失某些线程对变量的操作。
场景
以一个简单的促销活动为例,某购物网站举行促销活动,活动规则如下:
只要当前账户余额小于100元的,免费发50元。
-
假设线程1就是这个发放代金券的线程,线程1取得当前顾客的账户余额值A,
-
线程2也是一个发放代金券的线程,线程2在此时也取得了这个顾客的账户余额A,发现A<100,然后线程2就执行CAS操作,给当前账户进行加50元操作,此时账户余额为B,B=A+50。
-
与此同时,顾客在门店进行消费了50元,此时账户余额变为A,
-
然后线程1进行判断,发现A<100,所以进行CAS操作,比对了一下余额没有发现变化,它认为这段时间没有人带这个余额变量进行操作过,所以执行账户余额就变为A+50。
-
最后导致网站对顾客多执行了一次发放补助操作。试想如果对每个顾客的余额进行操作都遇到这种情景,那还不亏到哭。
解决
加版本号,对每个变量不仅仅记录他的值,而且记录发生时的版本(比如通过添加一个flag,来标记是否被处理过,或者记录时间等信息),将这两个值作为一个整体,让CAS进行判断。
这样每次比对时不只是比对值,还比对版本号,就不会发生ABA问题了。
典型实现
类似的处理还有数据库中对数据行的操作也可以采用加版本号来解决。
循环时间长开销大
如果循环CAS操作一直不成功,会给CPU带来很大开销。
只能保证一个共享变量的原子操作
解决
将多个共享变量合并成一个共享变量来操作,
典型实现
读写锁中,将读状态和写状态放在一个变量中,低16位表示写状态,高16位表示读状态。