文章目录
synchronized 关键字最主要的三种使用方式:
1.修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
同一个类里的两个synchronized 方法,在单例模式或者同一个对象的时候也会冲突,所以springboot 自动注入的时候默认是单利模式,在Service层的时候尽量一个service层只出现一个synchronized关键字修饰的方法
未被synchronized修饰的方法则不会被冲突
synchronized void method() {
//业务代码
}
2.修饰静态方法
也就是给当前类加锁,会作用于类的对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
synchronized void staic method() {
//业务代码
}
3.修饰代码块
指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
synchronized(this) {
//业务代码
}
在一个类里面增删改查最细的颗粒度,不能直接在方法上加锁,这样四个会竞争一个锁
public class UserServiceImpl {
private final Object addLock = new Object();
private final Object deleteLock = new Object();
private final Object updateLock = new Object();
private final Object queryLock = new Object();
public void add() {
synchronized (addLock) {
// 添加操作
}
}
public void delete() {
synchronized (deleteLock) {
// 删除操作
}
}
public void update() {
synchronized (updateLock) {
// 更新操作
}
}
public void query() {
synchronized (queryLock) {
// 查询操作
}
}
}
总结:
synchronized 关键字加到 static 静态方法和 synchronized(XX.class) 代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。
synchronized 关键字修饰代码块需要判断里面修饰的是对象,还是类
尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!
在JDK1.6之前,synchronized直接加重量级锁,JDK1.6之后得到了很好的优化。
synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁(锁膨胀)
对象一共16个字节,对象头占12个字节
- 1.A线程初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的线程id:0->A线程id,在线程的栈中生成锁记录(1.锁记录的地址2.对象指针(如果为重入则为null))),字面意思是“偏向于第一个获得它的线程”的锁。
- 此时对象从无锁状态变为偏向锁状态,执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会验证对象头中的线程id是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。
- 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
- 2.线程A还在继续持有锁并且未死亡,此时有第二个线程B加入锁竞争。
- CAS修改对象markword中的threadId为B线程Id,因为此时线程id已经不为期望值0所以修改失败。
- 进入偏向锁撤销的流程,等待全局安全点,此时判断持有锁的线程是否死亡。
- 如果已经死亡或者没有持有锁,则将对象头设置成无锁不可偏向状态,(标志位为001),然后CAS修改锁标志位01->00来获取轻量级锁。
- 未死亡则进入升级轻量级锁的过程。
- 持有锁的线程A的栈中生成锁记录,锁记录包含锁记录地址和对象指针,将对象头中Mark Word存储的信息与锁记录地址互换,将对象头中锁标志位改为00
- 偏向锁此时升级为轻量级锁(自旋锁)
- 在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位 01->00 并将对象头中Mark Word存储的信息与锁记录地址互换。如果成功就获得轻量级锁
这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
- 3.长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
- 显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改,1.6之后改为JVM自己控制)。
- 如果锁竞争情况严重,C线程达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。
- C线程CAS修改mark word 中锁的标志位为10,并为对象申请Monitor锁,将Owner设置为持有锁的线程,然后把对象头中Mark Word的锁记录地址替换为Monitor锁的地址。
- C线程完成这些操作后自己进入Monitor的EntryList BLOCKED。
- 当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则跟C线程一样直接将自己挂起(而不是忙等),等待将来被唤醒。
- 当轻量级锁持有的线程进入解锁流程,CAS将Mark Word值还给对象头,此时因为锁标志位已经被修改为10,锁记录已经被替换为Monitor记录,所以解锁失败,进入重量级锁解锁流程。
- 按照Monitor地址找到Monitor对象,并将原对象头存入Monitor,设置Owner为null,唤醒EntryList中BLOCKED的首节点线程进行非公平竞争。
- 4.当线程D尝试获取对象锁时先根据对象头中的Monitor地址找到Monitor,然后查找owner是否为Null。
- 如果为Null就直接获取锁并把Owner改为自己。
- 如果不为Null就进入cxq队列中,在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。
- 当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程唤醒。
- 因为在cxq队列中的线程会自旋抢夺锁,所以唤醒的线程不一定能抢到锁,因此重量级锁是不公平锁。
- cxq(竞争列表)(头插尾取)
cxq是一个单向链表。被挂起线程等待重新竞争锁的链表, monitor 通过CAS将竞争锁的线程包装成ObjectWaiter写入到列表的头部。为了避免插入和取出元素的竞争,所以Owner会从列表尾部取元素。- EntryList(候选者列表)(尾插头取)
EntryList是一个双向链表。当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程为OnDeck线程。- EntryList跟cxq的区别
在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。而EntryList中的线程都是被挂起的线程。
偏向锁
-
第一个线程占有的锁,如果两个线程竞争一定不是偏向锁
-
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态
-
hash code 是懒加载,不用到就不会计算
-
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
-
偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。
(此处线程id为操作系统给线程定义的id,与java中线程id不是一一对应的,不要尝试去理解)
101可偏向的对象,调用对象的hashcode()方法后会撤销偏向状态变成001,清除对象头里线程id等信息,并且把hashcode存入对象头,因为对象hashcode没有存储空间,轻量级锁会把hashcode存在线程栈帧的锁记录里,重量级锁会把hashcode存在monitor里,后期会还原回去,所以这两个锁不会受到影响
轻量级锁
重量级锁
Monitor监听器
ObjectMonitor() {
_header = NULL; //储存monitor关联对象的对象头
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0; // 重入次数
_object = NULL; // 储存monitor关联对象
_owner = NULL; // 储存当前持有锁的线程ID
_WaitSet = NULL; // 等待池:通过调用 wait(),将当前线程变为阻塞状态放到等待池
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 多线程竞争锁时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; // 锁池:处于等待锁block状态的线程,会被加入到该列表,如果被调用了notify(),则会将当前线程从 _WaitSet 移到 _EntryList
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
批量重偏向,批量撤销
1、当某个线程持有偏向锁,另一个线程想要获取锁时需要撤销锁。
2、撤销先尝试在不安全点使用CAS修改Mark Word为无锁状态,若还是无法撤销则考虑在安全点撤销,等安全点是比较低效的操作。
针对的是所有类的实例对象来说的,一个实例对象撤销偏向锁就算一次
- 批量重偏向:当撤销A对象偏向锁阈值超过20次,就会把所有classA的实例对象全部置位可偏向状态101
- 批量撤销:当撤销A对象偏向锁阈值超过40次,就会把所有classA的实例对象全部置位不可偏向状态(包括新创建的对象)001
wait()和notify()
https://blog.youkuaiyun.com/qq_43578385/article/details/124688200
锁消除 lock eliminate
有开关可以控制
例子1:
JIT即时编译器发现一段代码属于热点代码就会进行代码优化,如果方法里的局部变量不会逃离方法的作用范围,此时synchronized代码就会被优化掉
例子2:
public void add (String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}