并发编程学习笔记 一
线程中断
两种 interrupt() 方法执行情况:
- 当被中断的线程正处于 sleep join wait 状态
在执行 thread.interrupt() 后,都会终止线程的阻塞状态,但是不会将该线程的中断标志为置为 true。 - 当被中断的线程正处于正常运行的状态时
在执行 thread.interrupt() 后,并不会立即终止该线程的执行,而是会先将该线程的中断标志为置为 true,当该线程内部检测到中断标志为改变时,可以选择先去料理后事(如释放锁资源,连接资源等),再自行了断,这样可以优雅的终止线程,大大降低强制终止线程产生问题的几率。
设计模式之:两阶段终止模式的简单实现
public class Test1 {
public static void main(String[] args) throws InterruptedException {
TwoParseTermination twoParseTermination = new TwoParseTermination();
twoParseTermination.start();
Thread.sleep(3500);
twoParseTermination.stop();
}
}
class TwoParseTermination{
private Thread monitor;
public void start(){
monitor = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("进行线程结束前的料理后事工作(如释放锁资源)");
break;
}else{
try {
Thread.sleep(1000);
System.out.println("记录监控日志");
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
});
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}
线程的状态
-
操作系统中线程的五种状态:
(1)初始状态(2)可运行状态(就绪)(3)运行态(4)阻塞态(5)终止态 -
Java中定义的线程的六种状态
(1)NEW
(2)RUNNABLE
(3)BLOCKED
(4)WAITING
(5)TIMED_WAITING
(6)TERMINATED
synchronized原理
- java的对象头
在 32 位虚拟机中,java的对象头共占有 8 个字节,其中有 4 个字节的 MarkWord 和 4 个字节的 KlassWord。 - MarkWord格式
Mark Word (32 bits) | State |
---|---|
identity_hashcode:25 /age:4 /biased_lock:1 /lock:2 后三位001 | Normal (正常) |
thread:23 (线程唯一标识)/ epoch:2 age:4 biased_lock:1 lock:2 后三位101 | Biased(偏向锁) |
ptr_to_lock_record:30 (指向线程栈中的锁记录) /lock:2 后二位00 | Lightweight Locked (轻量级锁) |
ptr_to_heavyweight_monitor:30 (指向 Moniter 对象) /lock:2 后二位10 | Heavyweight Locked(重量级锁) |
lock:2 后二位11 | Marked for GC (标记为垃圾回收) |
重量级锁
- 在 sychronized 给对象上锁(重量级锁)之后,MarkWord 会指向操作系统中的对象 Moniter。
- Moniter
Moniter 翻译为监视器或管程,大体上有三部分组成:
(1)Owner :一个管程只能有一个 Owner ,代表当前正占有锁的线程,一开始为 null 。
(2)EntryList:表示一个存放被阻塞线程的集合,也就是常说的阻塞队列。
(3)WaitSet:存放之前获取过锁,但由于某些原因进入 WAITING 状态的线程。 - 自旋优化:在重量级锁竞争的过程中,当前线程在检测到锁已被占用后不会立即进入阻塞队列,而是占用 cpu 再进行若干次尝试,失败后才会进入阻塞队列,以减少线程上下文切换成本。
注意:因为线程需要占用 cpu 不断尝试获取锁,所以自旋优化仅仅使用与多 cpu。
轻量级锁
-
应用场景:如果一个对象在多线程环境下需要加锁,但是加锁的时间时错开的,也就是没有竞争,那么可以使用轻量级锁来优化(避免使用 Moniter 导致开销大),轻量级锁对用户是透明的,语法仍然是 synchronized。
-
工作流程:
(1)当调用 sychronized 给对象加锁时,每个线程的栈帧都会包含一个锁记录(lock record)对象,锁记录内部可以存储锁对象的 MarkWord。
(2)尝试给对象加锁,让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录的地址,以及标记 Mark Word 最后两位为 00 ,标志该对象已被加上轻量级锁。 -
cas 交换失败的两种情况
(1)当前线程想要执行 cas 交换时,发现对象头的 Mark Word 最后两位为 00 ,并且不是自己加的锁,说明已经有其他线程加上了锁,自己执行锁升级流程。
(2)当前线程想要执行 cas 交换时,发现对象头的 Mark Word 最后两位为 00 ,但是发现是自己加的锁,自己执行锁重入流程,添加一条 Lock Record 作为重入的计数,此时锁记录中的 Object reference 仍然指向锁对象,但存储 Mark Word 的位置为 null。 -
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
成功,则解锁成功
失败,说明轻量级锁进行了锁升级或已经升级为重量级锁,进入重量级锁解锁流程 -
锁升级
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有
竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。即为锁对象申请 Moniter ,让锁对象的 MarkWord 指向 Moniter,然后自己进入 EntryList 阻塞。
偏向锁
-
应用场景:当轻量级锁没有竞争时,自己每次重入都要执行 CAS 操作,并创建栈帧。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
-
偏向锁的撤销
(1)调用对象的 hashcode:因为偏向锁没有额外存储对象 hashcode 的空间,只能将对象头改为正常状态,也就是取消偏向锁状态。
(2)当有其他线程使用锁(无竞争):会使偏向锁升级为轻量级锁。
(3)调用 wait / notify 方法:因为该方法属于重量级锁的范围,自然会撤销偏向锁。 -
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象
的 Thread ID,当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至
加锁线程。 -
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象
都会变为不可偏向的,新建的对象也是不可偏向的