【后端修行之JUC】关于重量级锁、轻量级锁和偏向锁

本文用于记录学习黑马JUC课程后,个人认为有价值的笔记,核心内容均围绕标题展开。既做一个学习的记录,同时也做一个沟通交流,欢迎各位大佬互动~

共享带来的问题

共享资源过程发生多线程的上下文切换,导致指令交错,造成线程安全的问题(字节码交错)

临界区: 代码块, 该代码块中对共享资源产生多线程读写操作

竞态条件(竞态情况Race Condition): 情况, 多个线程在临界区内执行,由于代码执行序列不同而导致结果无法预测的情况

​​​​​​​Synchronized解决方案(重量级锁)

背景: 阻止临界区的竞态条件发生,有多种手段:①阻塞式:synchronized, Lock; ②非阻塞式: 原子变量

原理: Synchronized, 对象锁, 它采用互斥的方式让同一时刻至多只有一个线程能持有[对象锁], 其他线程想再次获取这个对象锁时,就会被阻塞住. 这样可以保证拥有锁的线程可以安全执行临界区内的度挨骂, 不用担心上下文切换.

功能: Java 的互斥和同步由synchronized关键字完成, 但两者有区别:

互斥: 它能避免临界区的竞态条件发生, 同一时刻只有一个线程执行临界区代码.

同步: 它是指由于线程执行的先后,顺序不同, 需要一个线程等待其他线程运行到某个点的情况.

加深理解小案例:①synchronized加for循环外面和里面区别(双线程都有for循环); ②synchronized加不同的对象? ③synchronized只加一个对象,另一个不加? ④线程八锁问题;

面相对象的改进: 锁可以加在成员对象(加成员方法上,代表加锁this对象), 也可以加在类对象(加静态方法上,代表加锁类对象)

​​​​​​​线程安全分析

三类变量的线程安全问题: 静态变量, 成员变量, 局部变量.

静态变量和成员变量的线程安全性(是否被共享以及是否被读写?):

①如果没有变量没有被共享, 则线程安全;

②如果被共享,但只有读操作,则线程安全; ③如果被共享,但有读写操作, 该代码块为临界区, 存在线程安全的问题.

局部变量线程安全性

①局部变量本身是线程安全的(不会被多个线程所共享, 局部变量在栈帧中, 栈帧在虚拟机栈中, 虚拟机栈是线程私有的);

②局部变量引用的对象则未必: 引用的对象没有逃离,线程安全(引用没有暴露)/引用对象逃离方法作用范围,线程不安全(引用暴露);

注: 方法访问修饰符一定程度上也能保护线程安全(private修饰的方法子类不能重写, 如果可以重写可通过子类重写新建线程并对局部变量进行访问读写, 线程不安全.)

常见线程安全类:

String, Integer, StringBuffer, Random, Vector(线程安全的list实现), Hashtable(线程安全的map实现, java,util.concurrent包下的类;

这里的线程安全指的是, 多个线程调用同一个实例的某个方法时, 是线程安全的(针对同一个共享变量, 方法是原子的, 但是方法的组合不是原子的)

不可变类线程安全性(String, Integer): 因其内部的状态不可改变,因此他们的方法都是线程安全的。

​​​​​​​Monitor(锁/监视器/管程)--重量级锁

Monitor工作原理: (涉及线程123...n, object对象, Moniter(其中包含属性WaitSet, EntryList, Owner))

线程1运行到同步代码块时(synchronized(object){}), 会确认object是否和操作系统层面的Moniter对象是否关联(关联指:Object对象头的Mark Word部分是否有指针指向Moniter对象), 如果没关联, 则线程1拥有了锁, Monitor对象属性的Owner指向线程1.

如果线程2, 线程3, 同样执行到同步代码块, 同样会看object是否与Monitor对象关联, 如果锁被其他线程占用(Object与Monitor关联), 则线程从可运行状态进入”阻塞状态”, 同时Monitor的EntryList属性指向不同的阻塞线程.

注意:①synchronized必须是进入同一个对象的monitor才有上述效果;②不加synchronized的对象不会关联监视器, 没有上述效果.

​​​​​​​轻量级锁(针对重量级锁的优化)

轻量级锁(对使用者透明, 仍是synchronized语法): 如果一个对象虽然有多线程访问, 但多线程访问的时间是错开的(无竞争), 那么可以使用轻量级锁来优化.

过程:

创建锁记录(Lock Record)对象(包含指向对象的指针, 以及指向锁记录的指针(后续用于cas操作并与对象Mark Word交换/计锁重入数)).

②让锁记录的Object Reference指向锁对象, 并尝试用cas替换Object中的Mark Word, 讲Mark Word值存入锁记录;

③如果cas成功, 对象头重存储锁记录和状态00, 表示该线程给对象加锁;

④如果失败有两种情况:<1>其他线程持有该Object的轻量级锁, 表示有竞争, 进入锁膨胀过程; <2>如果是自己线程执行了synchronized锁重入, 那么再添加一条Lock Record作为重入的计数.

⑤退出synchronized代码块时(解锁), 如果有取值为null的锁记录表示有重入,这时重置锁记录, 表示重入计数减一;

⑥当退出synchronized代码块时(解锁), 如果锁记录的值不再为null,这时将cas将Mark Word的值恢复给对象头:<1>成功,则解锁成功; <2>失败,则说明轻量级锁已经进行了锁膨胀或已升级为重量级锁, 进入重量级锁解锁流程.

锁膨胀: 线程在尝试加轻量级锁的过程, cas无法操作成功, 一种情况就是其他线程为此线程加上了轻量级锁, 这时需要进行锁膨胀, 把轻量级锁变成重量级锁.

过程:

①线程1尝试进行轻量级加锁, 线程0已对对象加了轻量级锁;

②线程1加轻量级锁失败, 进入锁膨胀流程: <1>为Object申请Monitor锁, 让Object指向重量级锁地址; <2>然后自己进入Monitor的EntryList, 进入阻塞状态;

③解锁过程: 线程0退出代码块解锁, 尝试cas将Mark Word恢复给对象头, 失败. 这时进入重量级解锁流程, 按照Object中Monitor地址找到Monitor对象, 设置Owner属性为null, 环形EntryList中的BLOCKED线程

自旋优化: 重量级锁竞争, 还会通过自旋进行优化, 自旋成功(持有锁线程退出同步块, 释放锁), 当前进程可以避免阻塞. 自旋失败则进入阻塞状态.

特点: Java6后自旋自适应(无需设置次数, 自动根据历史情况调整); 自旋占用cpu, 单核cpu自旋浪费, 多核cpu才能发挥优势; Java7后不能控制是否开启自旋功能.

​​​​​​​偏向锁

偏向锁: 是对轻量级锁无竞争时, 每次重入仍然需要cas操作的优化. 第一次使用cas将线程id设置到对象Mark Word, 后续检查对象中的id为自身线程id则不用重新cas, 该锁就偏向某个线程

偏向状态:

撤销偏向锁:

①调用对象hashCode: 原本对象Mark Word存的是线程id, 调用后会被哈希码覆盖, 导致偏向锁被撤销; 轻量级锁会在锁记录记录哈希码, 重量级锁会在Monitor中记录哈希码, 因此没问题.

②其他线程使用对象: 偏向锁升级为轻量级锁;

③调用wait/notify: 只有重量级锁有这个方法;

批量重偏向: 对象被多个线程访问但无竞争, 偏向t1线程的对象就会重新偏向t2(重置对象的ThreadID). 撤销锁阈值到20次,批量重偏向

批量撤销: 撤销锁阈值到40次, 整个类的所有对象都会变为不可偏向, 新建的对象也会变成不可偏向

锁消除: 对于局部变量加锁, 会优化掉锁, 减少对性能的影响.

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值