synchronized 解决方案
互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量
synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有
【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区
内的代码,不用担心线程上下文切换
synchronized实际上是用对象锁保证了临界区内代码的原子性,临界区内的代码是不可分割的,不会被线程切换所打断
考察synchronized锁住的是哪个对象
1.锁住的是n1-(this)
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
2.若synchronized加在静态方法上,则锁住的是当前类的运行时类对象(.class)
变量的线程安全分析:
1.成员变量和静态变量是否线程安全?
如果它们没有被共享,则是线程安全的
如果它们被共享了,根据它们的状态是否能被改变,又分为两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全
2.局部变量是否线程安全
局部变量是线程安全的
但局部变量引用的对象则未必
如果该对象没有逃离方法的作用访问。它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全(如使用return)
3.线程安全的多个方法组合在一起不一定线程安全,需要在组合方法上添加线程安全保护
4.不可变类线程安全
如String、Integer都是不可变类,内部的状态不可变,因此线程安全
java对象的结构:
1.对象头:
①:Mark Word:hashcode、分代年龄、状态、加锁状态位
②:Klass word:类型指针(什么类型的对象)
2.对象体:
成员变量的信息
Java 对象头:
以 32 位虚拟机为例
64 位虚拟机 Mark Word
Monitor概念
Monitor被翻译为监视器或者管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor的指针
Monitor结构如下:
1.刚开始Monitor种Owner为null
2.当Thread-2执行synchronized(obj) 就会将Monitor的所有者Owner置为Thread-2,Monitor种只能有一个Owner
3,在Thread-2上锁的过程种,如果Thread-3,4,5也来执行synchronized(obj) ,就会进入EntryList BLOCKED
4.Thread-2执行完同步代码块的内容,然后唤醒EntryList种的等待线程来竞争锁,竞争是非公平的
5.图中WaitSet种的Thread-0,Thread-1是之前获得过锁,当条件不满足WAITING状态的线程,后面讲wait-notify
时分析
**注意:**
synchronized必须时进入同一个monitor才有上述效果
不加synchronized的对象不会关联监视器,不遵从以上规则
synchronized原理进阶:
轻量级锁:
使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁
轻量级锁对使用者是透明的,语法仍然为synchronized
1.创建锁记录(Lock Record),每个线程的栈帧都会包含一个锁的记录,内存可以存储锁对象的Mark Word
2.让锁记录中Object reference 指向锁对象,并且尝试用cas替换Object中的Mark Word,将Mark Word的值存入锁记录(若Mark Word值已经被其他线程所修改则替换失败)
3.如果cas替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁
如果cas失败:
1.其他线程已经持有了该Object的轻量级锁,此时表明有竞争,进入锁膨胀过程
2.如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数<br />![1646820022734.png](https://cdn.nlark.com/yuque/0/2022/png/26737039/1647331695203-2b372579-5ee7-4efc-ae87-c2252e2679c6.png#clientId=u46ad077f-3ef3-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=lrT5o&margin=%5Bobject%20Object%5D&name=1646820022734.png&originHeight=359&originWidth=597&originalType=binary&ratio=1&rotation=0&showTitle=false&size=109184&status=done&style=none&taskId=u75c465ef-1c37-456f-8a81-ec82f3f14d3&title=)
解锁:
1.当退出synchronized代码块(解锁时) 如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减1
2.当退出synchronized代码块时,锁记录的值不为null,这是使用cas将Mark Word的值恢复给对象头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2.锁膨胀:
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
1.当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
2.这时Thread-1加轻量级锁失败,进入锁膨胀流程
①:即为Object对象申请Monitor锁,让Object指向重量级锁地址(后两位数字变为10)
②:然后自己进入Monitor的EntryList BLOCKED
3.当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败,这时会进入重量级解锁流程:即按照Monitor地址找到Monitor对象,将Owner设置为null,唤醒EntryList中的BLOCKED线程
3.自旋优化:(比较适合多核CPU)
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持有锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞 (阻塞会发生上下文切换,消耗性能)
自旋成功的情况:
自旋失败的情况:
注意:
1.在java6之后自旋锁是自适应的,比如对象刚刚的一次自选操作成功过,那么认为这次自旋成功的可能性会高,就会多自旋几次;反之,就是少自旋甚至不自旋,比较只能
2.自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
3.java 7 以后不能控制是否开启自旋功能
4.偏向锁:
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作(性能损耗)
java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就没有表示竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
4.1 偏向状态:
回忆对象头格式:
一个对象创建时:
1.如果开启了偏向锁(默认开启),那么对象创建后,markword值的后三位为101(biased_lock字段表示是否开启偏向锁,1为开启),此时他的thread、epoc、age都为0
2.偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加参数来禁用延迟
3.如果没有开启偏向锁,那么对象创建后,markword值为001,此时他的hashcode、age、都为0,第一次用到hashcode时才会赋值
4.处于偏向锁的对象解锁后,线程id仍存储于对象头中
注意:
当我们的程序本身就是线程很多,那么偏向锁就不适用,可以通过参数来禁用:
添加VM参数: -xx:-UseBiasedLocking来禁用偏向锁
4.2 撤销偏向锁—-调用对象的hashcode方法
当我们调用对象的hashcode方法就会禁用掉偏向锁
原因:通过对象头的结构可知,Mark Word 的空间是有限的,当我们开启偏向锁,就会存储线程id,而没有空间存储hashcode,所以我们调用hashcode方法,就会关闭偏向锁,清除掉线程id替换为hashcode
为什么轻量级锁和重量级锁不会有上述问题?
- 轻量级锁会存储在线程栈帧的锁记录中
- 重量级锁会将hashcode存储在Monitor对象中,解锁时再进行还原
- 只有偏向锁没有额外的空间,所以会有上述问题
4.3 撤销偏向锁—-其他线程使用对象
当其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
4.4 撤销偏向锁—-调用wait/notify
只有重量级锁才有这两个方法,所以调用会将偏向锁或轻量级锁转换为重量级锁
4.5 批量重偏向
如果对象虽然被多个线程访问,但是没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID(线程ID)
当撤销偏向锁的阈值超过了20次,jvm会觉得是不是偏向错了呢?于是会给在这些对象加锁时重新偏向至加锁线程
4.6 批量撤销
当撤销偏向锁的阈值超过了40次,jvm会觉得自己偏向错了,根本就不该偏向,就会把整个类的所有对象变为不可偏向的,新建的对象也是不可偏向的
5.锁消除
java运行时有一个JIT即时编译器,会对java字节码进一步优化,反复运行的代码超过一定的阈值就会进行即时优化,当发现变量不会存在线程安全问题,JIT就会把多余的synchronized去掉
6.锁的粗化
public void test1(){
for(int i=0;i<1000;i++){
synchronized(Test.class){
sout("hello");
}
}
}
锁粗化后:扩大锁的范围,避免反复加锁和释放锁
public void test1(){
synchronized(Test.class){
for(int i=0;i<1000;i++){
sout("hello");
}
}
}