本文用于记录学习黑马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次, 整个类的所有对象都会变为不可偏向, 新建的对象也会变成不可偏向
锁消除: 对于局部变量加锁, 会优化掉锁, 减少对性能的影响.