一、引言
1、volatile能不能保证线程安全?
public class Test {
public static int i = 1;
public static void main(String[] args) {
i++;
}
}
// 字节码
0 getstatic #2 <com/example/demo/Test.i : I>
3 iconst_1
4 iadd
5 putstatic #2 <com/example/demo/Test.i : I>
由JUC(一)JMM内存模型可知,volatile可以保证可见性和有序性,那么使用volatile可以达到线程安全吗?上面代码是i++的字节码,假设有这样一种场景,由于volatile不能保证原子性,就会导致出现线程不安全:
序号 | 步骤 | 线程1工作内存 | 线程2工作内存 | 主内存 | 备注 |
---|---|---|---|---|---|
1 | 线程1执行,执行getstatic #2; 线程2不执行 |
计算结果0 i = 1 |
计算结果0 i = 0 |
i = 1 | - |
2 | 线程1执行,执行iconst_1; 线程2不执行 |
计算结果0 i = 1 |
计算结果0 i = 0 |
i = 1 | - |
3 | 线程1执行,执行iadd; 线程2不执行 |
计算结果2 i = 1 |
计算结果0 i = 0 |
i = 1 | iadd只进行计算,还未进行赋值 |
4 | 线程1不执行; 线程2执行,执行getstatic #2 |
计算结果2 i = 1 |
计算结果0 i = 1 |
i = 1 | - |
5 | 线程1不执行; 线程2执行,执行iconst_1 |
计算结果2 i = 1 |
计算结果0 i = 1 |
i = 1 | - |
6 | 线程1不执行; 线程2执行,执行iadd |
计算结果2 i = 1 |
计算结果2 i = 1 |
i = 1 | - |
7 | 线程1不执行; 线程2执行,执行putstatic #2 |
计算结果2 i = 1(失效) |
计算结果2 i = 2 |
i = 2 | 由于volatile的可见性,给i赋值后, 会同步其他线程i已经失效 |
8 | 线程1执行,重新读取i; 线程2不执行 |
计算结果2 i = 2 |
计算结果2 i = 2 |
i = 2 | 线程1重新从主内存中获取i的值 |
9 | 线程1执行,执行putstatic #2; 线程2不执行 |
计算结果2 i = 2 |
计算结果2 i = 2 |
i = 2 | 线程1将计算结果赋值给i, 导致执行了两次i++,结果还是2 |
2、竞态条件与临界区
- 如上文的场景,当多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
- 在临界区中使用适当的同步就可以避免竞态条件;如:synchronized、Lock。
二、synchronized
1、非静态方法锁和静态方法锁的区别
public class Test {
private int i = 1;
private static int j = 1;
// 方法锁
public synchronized void add() {
i++;
}
// 静态方法锁
public synchronized static void addStatic() {
j++;
}
}
上述代码等价于
public class Test {
private int i = 1;
private static int j = 1;
public void add() {
synchronized (this) {
i++;
}
}
public static void addStatic() {
synchronized (Test.class) {
j++;
}
}
}
2、锁分类
由JVM(三)堆与对象可知,对象头中的MarkWord会有位置标识锁状态:
<1> 偏向锁
- 当只有一个线程持有锁,并且该对象没有调用过系统的hashcode方法,则它可被设置成偏向锁
- 对象被设置成偏向锁后,再调用系统的hashcode方法,则膨胀为轻量锁或重量锁
<2> 轻量锁
Lock Record 结构- 在线程的栈帧中有Lock Record存储锁对象的MarkWord
- 竞争线程会通过自旋尝试获取锁
- 自旋结束,膨胀为重量锁
<3> 重量锁
Monitor结构- 如果使用synchronized给对象加了重量级锁,该对象的MarkWord就会指向Monitor。
- obj的MarkWord会存储到Monitor中
- Owner指向当前持有锁的线程
- 如果有新的线程想要获取锁,会进入EntryList列表进行等待
- 调用锁对象wait方法的线程会进入WaitSet中,等待被notify唤醒,唤醒后进入EntryList
<4> 锁膨胀过程
锁膨胀过程3、其他
<1> 为什么要有偏向锁、轻量锁?
- 在多线程环境下,为了避免未抢到锁的线程空转,浪费资源。需要对这些线程进行阻塞,当占用锁的线程释放锁后,需要唤醒另一个竞争到锁的线程。
- 在JVM中,这些操作都需要切换到内核态实现。
- 重量锁涉及到用户态、内核态的切换,比较消耗资源。
- 在单线程和只有两个线程竞争的情况,使用到偏向锁、轻量锁,来优化性能。
<2> 重入
- 锁重入,就是支持正在持有锁的线程再次获取锁,不会出现自己锁死自己的情况。
- 在竞争锁时,如果发现锁已经被持有了,并且持有人是自己,那么就做一个标记。
- 轻量锁重入时会新增一个Lock Record,只是这个Lock Record不再存储锁的MarkWord。
- 重量锁重入时,会将monitor的count+1。
- 解锁时,重量锁会先将count-1;count == 0表示释放锁。
synchronized (this){
synchronized (this){
}
}
<3> 公平锁、非公平锁
- 持有锁的线程释放锁后,EntryList中的线程开始竞争锁;
- 多个线程按照申请锁的顺序去获得锁,就是公平锁;
- 多个线程按照系统的调度获取锁,就是非公平锁。
网友评论