管程
# i++线程安全问题
i++的字节码:
getstatic i//获取静态变量i的值
iconst_1 //准备常量1
iadd //自增
putstatic i//将修改后的值存入静态变量
2
3
4
Java的内存模型,完成静态变量的自增,需要再主存和工作内存中数据交换:
问题:多个线程访问共享资源,多个线程对共享资源的多线程读写操作发生指令交错,出现问题!
临界区:一段代码块内存在共享资源的多线程读写操作
竞态条件:多个线程在临界区内执行,由于代码块执行序列不同导致结果无法预测
解决方案:
- 阻塞式:synchronized,lock
- 非阻塞:原子变量
案例代码:
public class synchronizedtest1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
class Room {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized void decrement() {
counter--;
}
public synchronized int getCounter() {
return counter;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 局部变量的线程安全情况
局部变量在每个线程的栈帧内存中都创建了副本,不存在共享,不存在线程安全问题
当list为局部变量,多线程下,每个栈帧都有这个list副本
局部变量出现线程安全问题:该对象逃离方法的作用范围
解决方式:final & private
实例代码:
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
//list在方法里申明的,新线程和原线程共享了list
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 悲观锁&乐观锁
悲观锁:
java里面悲观锁有synchronized和ReentrantLock;
mysql里面悲观锁有像select...for update
select...for update 一定跟上where id=?的条件,且 id 字段一定是主键或者唯一索引,否则锁全表;
优点:锁的是代码块,可以锁住多个变量;
缺点:因为是独占锁,加锁释放锁都会有开销,且一个线程持有锁会导致其它所有需要此锁的线程挂起,上下文切换开销大,甚至导致死锁;
乐观锁:CAS或版本号机制(自旋+重试)
针对一个变量,首先对变量进行修改,比较它的内存值和期望值,相同则将修改后的新值覆盖内存,否则不处理;
CAS原理:包含了 compare和 swap两个操作,它的原子性是由CPU支持的原子操作;
优点:轻量级锁,避免线程切换的开销,更细粒度的控制;
缺点:
- ABA问题
- 只能对单一变量加锁
- 自旋操作导致额外开销
使用场景:
高并发竞争场景,避免重试开销,直接选用悲观锁;
并发场景不激烈,细粒度的共享变量控制乐观锁;
# Sysnchronized和lock区别
sysnchronized是jvm层面的锁,隐式获取和释放锁;
lock是java api层面的,实现了顶层接口Lock,显式获取和释放,可以根据tryLock判断其状态,支持可中断获取锁,超时获取锁,公平锁等特性;
如果没有特殊要求,使用synchronzied(有优化,性能也不错),如果需要判断锁状态,或者像实现阻塞队列那样需要多个条件变量的话可以使用ReentantLock;
# Sysnchronzied原理
通过javap工具查看class文件可以分析出:
同步块的实现主要依靠monitorenter和monitorexit指令;
同步方法依靠修饰符上的ACC_SYNCHRONIZED完成;
本质都是获取对象的monitor,这个过程是排他的,获取失败的线程被阻塞在同步块和同步方法的入口处,进入BLOCKED状态;monitor的EntryList里记录了阻塞线程ID,在进入同步块的线程执行到Monitor.Exit后,通知EntryList里的线程出队列;
Monitor结构:WaitSet+EntryList(阻塞队列,非公平竞争)+Owner(只能一个)
monitorenter:每个java对象头都关联一个Monitor对象,synchronized上锁之后,该对象头的mark word被设置指向monitor对象的指针,将monitor中的owner指向获取锁线程的锁记录;
monitorexit:将锁对象重置,唤醒EntryList;
同步代码块出现异常,能不能释放锁?
能,通过异常表,有异常,多一次monitorexit过程,确保锁释放!
# Synchronized优化
轻量级锁
膨胀第一步:线程中的锁记录地址替换为对象的markword
无需monitor锁,用的线程栈的锁记录,cas用锁记录替换markword;
使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的,没有竞争
案例代码:可重入锁
static final Object obj = new Object(); public static void method1() { synchronized( obj ) { // 同步块 A method2(); } } public static void method2() { synchronized( obj ) { // 同步块 B } }
1
2
3
4
5
6
7
8
9
10
11
12执行流程:
一个方法对应一个栈帧,每个线程的栈帧都包含一个锁记录结构(lock record地址&状态00 + Obj Ref),内部存储了锁定对象的Mark Word
对象头+对象体
对象头:mark word:hash码+分代年龄+加锁状态位,class word:类型指针
对象体:成员变量
用cas替换对象的Mark Word,将值存入锁记录
若cas成功,对象头中存储了锁记录地址和状态 00 ,表示由该线程给对象加锁;
若cas失败,可能锁存在竞争,进入锁膨胀过程 || 可能锁重入,再添加一条锁记录作为重入计数
锁重入:
解锁时:
如果锁记录取值为null,表示有重入,重置锁记录,重入计数减一;
如果不为null,cas将Mark Word的值恢复给对象头(若cas失败:说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程)
锁膨胀
场景:新添加线程尝试加轻量级锁的过程中,CAS 操作无法成功,因为有其它线程为此对象加上了轻量级锁,这时需要进行锁膨胀,将轻量级锁变为重量级锁
流程:
为对象申请Monitor锁,让对象的Mark Word引向Monitor,新添加线程进入Monitor的EntryList
获得锁的线程解锁时,cas将Mark Word 的值恢复给对象头,失败。进入重量级解锁流程,即按照Monitor地址找到 Monitor对象,设置Owner为null,唤醒 EntryList 中 BLOCKED 线程
自旋优化
膨胀第二步:当开始有线程竞争锁时,原本应该进入阻塞队列中,但是有可能占有锁的线程即将释放锁,且自旋的线程数量不多,将升级为自旋锁;
- 场景:避免新添加线程直接进入阻塞队列,出阻塞队列后线程切换造成上下文切换
- 原理:重量级锁竞争的时候,使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放锁),这时当前线程就可以避免阻塞。
偏向锁(jdk15默认禁止)
场景:轻量级锁没有竞争时,每次重入需要CAS操作
禁止原因:偏向锁带来的加锁时性能提升从整体上看并没有带来过多收益(维持锁的成本过高)
原理:用ThreadID替换markword,下次cas操作不需要从markword中检查,直接检查ThreadID是否是自己
重量级锁;
膨胀第三步:自旋锁达到自旋次数没有拿到锁后,升级为重量级锁;
# 原子性如何保证?
总线锁保证;
缓存锁保证;
总线锁定把CPU和内存之间的通信锁住了,锁定期间,其它处理器不能操作其它内存地址的数据,所以总线锁定的开销大;
频繁使用的内存会缓存在三级缓存中,原子操作可以直接在处理器内部缓存中进行,无需总线锁;
缓存锁定:内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作写回内存时,修改的是内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性会阻止同时修改由两个以上处理器缓存的内存区域数据,当其它处理器回写已被锁定的缓存行的数据时,会使缓存行无效;
# 实例锁和类锁
实例锁:每个实例在JVM中有自己的 引用地址 和 堆内存空间,实例上加锁和其它实例就没有关系;
类锁:类信息存在JVM方法区,整个JVM只有一份,方法区是所有线程共享的,所以类锁是所有线程共享的;
类锁:静态变量,静态方法,xxx.class