Java中的锁机制——中断与Synchronized
一、程序中锁的概述
在多线程的并发下为了保证资源的安全性我们需要对资源的相关操作进行限制,这个限制就是通过锁机制实现,锁本质上就是通过在系统对一个资源做出标记,这个标记代表着一定的读写权限,那个线程拿到了这个标记其他的线程就需要进行等待。从锁的性能来讲我们可以将锁分为悲观锁、乐观锁;从使用的表现形式上来看又可以分为偏向锁、轻量级锁、重量级锁,为了最求达到最佳的性能,通常这种锁会搭配进行使用。例如synchronized关键字从1.6之后默认使用的是偏向锁、之后根据竞争情况又会升级为轻量级锁、重量级锁。针对不同的场景有会有读写锁、独占锁、共享锁等,我们也可以根据自己的规则利用抽象同步队列AQS、LockSupport定义出来自己的锁机制
锁可以说是为了协调各个线程之间对数据的读写操作,乐观锁是通过CAS算法不断的进行比对判断原来的值是否被改变过,如果没有被改变过则对这个线程的值进行赋值操作,如果要是值被改变过不符合预期那么则将会进行自旋操作直至数据被修改成功。这个过程中不存在线程的上下文切换,用的是CPU轮询,直至赋值成功。悲观锁的就是通过在系统中做一个标记,拿到这个标记的线程可以执行相关的操作,其他的线程进行等待,这时候线程将会被挂起,在这个过程中线程存在内核态与用户态的上下文切换,是会影响系统性能的。
二、中断机制
中断机制遵循的一个原则是,其他的线程可以设置一个线程中中断转态但是也就仅仅是设置而已,并不会造成线程的停止,被设置为中断状态之后如何操作取决于线程自己。![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pSDiUXOE-1660484335871)(F:\typroa\aimages\image-20220814173815380.png)]](https://img-blog.csdnimg.cn/92a2c555083a4e0fb74a161db63ce0c1.png)
如果线程处理被阻塞状态(slep/wait/join等状态)在别的线程调用点前对象的Interrupt方法时候那么线程将立即退出被阻塞转态,并抛出一个InterruptedException异常,想要解决这个问题就是在异常捕获中在重新的设置一下中断标识
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("线程被设置了中断被标识");
break;
}
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
//sleep中的线程被interrupt打断,发生异常,线程的中断标识将会被清除
//想要实现线程的正常终止,就需要在异常中重新设置异常标识
//异常的报错信息可能会打印出来可能也不会打印出来,机会均等,此取决于CPU调度
System.out.println("sleep发生了异常");
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println("---hello---");
}
//Thread.interrupted()会返回当前线程的终端标记,并且对终端标记进行清除
System.out.println("Thread.interrupted():"+Thread.interrupted());
System.out.println("Thread.interrupted():"+Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
System.out.println("设置了线程1的中断标识");
}
}
上面出现了三个与中断相关的方法,isInterrupted()是检查当前的中断状态,interrupted()用来返回当前的终端状态并且清楚当前的中断状态,他们的底层调用的都是同一个Native方法,只是传参不同而已
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
三、synchronized锁机制
Java中Synchronized是重量级锁,基于计入和退出Monitor对象实现,在编译时会将同步块的开始位置插入monitor enter指令,在结束的位置插入monitor exit指令。当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了即获取到了锁,并且在Monitor的owner中存放当前线程的Id,这样他讲处于锁定状态,除非退出同步代码块,否则其他线程无法获取到这个Monitor。但是JDK1.6之后有所改进加入了偏向锁、轻量级锁,不过JDK15之后有逐步取消偏向锁,原因是他的执行消耗较大。在锁升级的过程中,我们的监视器对象,关于锁的升级过程中的信息是存储在对象头中:![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tTujHaoJ-1660484335873)(F:\typroa\aimages\image-20220814194013020.png)]](https://img-blog.csdnimg.cn/6059b8972b01472a8b56156df1941ed0.png)
1、对象头
一个对象基本由对象头(对象标记、类源信息)实例数据、对齐填充组成,这里面我们关注的主要是对象头中对象标记的信息,里面有与此对象垃圾回收相关的GC信息以及锁相关的信息,还有hashCode。锁升级的过程实际上就是更改锁标记位置,以及对应指针的过程。
2、偏向锁
偏向锁主要针对一个资源几乎总是被一个线程访问的场景,他就会记录当前这个线程的ID,并且不会主动的释放,直到有其他的线程过来竞争。其他的锁过来竞争时候会判断当前偏向锁中记录的是否有线程ID,如果有的话这个线程的是否已经执行完毕,如果当前占有偏向锁的线程已经执行完毕则直接获取此偏向锁。如果当前占有偏向锁的线程尚未执行完毕则会在最近的全局安全点(类似于JVM中的Stop The Word),更改锁标记为轻量级锁,给其他的线程提供竞争的机会。
3、轻量级锁
轻量级锁实际上就是我们说的乐观锁或是CAS,底层通过调用UnSafe类来进行操作,他不会引起上下文切换。但是也并不是说乐观锁就一定是好的一直是好的,如果资源的竞争激烈并且占用时间较长的话那么轻量级锁带来的性能消耗更大。轻量级锁更加适用的场景是竞争的线程几乎总是你进我出,有竞争但是并不不激烈。CAS算法实际上就是自旋,至于说自旋多少次各有不同,不过后面有一种算法就是自动调整自旋的次数,如果第一次就通过CAS获取成功程序就会认为下次的自旋应该也会成功就适当的增加自旋的次数,如果自旋获取失败程序则会降低自旋次数。当自旋失败达到一定的次数程序就会进行锁升级,升级的时间点也是全局安全点。
4、重量级锁
重量级锁会带来线程的上下文切换,获取锁失败的线程将会进入到阻塞队列中,如果其他的线程释放了锁资源那么在阻塞队列中的线程将会重新争夺锁资源,争夺的策略根据是否是按照先来先得可以分为公平锁与非公平锁,也可以获得锁的重入次数等信息。
5、HashCode与锁升级
在对象头中我们可以看到,在无锁的时候有31bit进行hashcode的存储,但是有锁之后此空间被锁相关的详细霸占。hashCode虽然不强制要求固定不变但是强烈建议固定不变,那么当发生锁升级的时候hashCode如何存储
- 在无锁的状态下,MarkWord中可以存储对象的indetity hash code值,当对象的hashCode方法第一次被调用的时候,JVM会生成对应的indentity hash code值并将改值存储到MarkWord中
- 对于偏向锁,如果一个对象的hashCode方法已经被调用过一次后这个对象不能被设置偏向锁,锁会直接升级为重量级锁,当一个对象整处于偏向锁状态又收到需要计算一致性哈希码请求时,他的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。代表重量级锁的ObjectMonitor类李有字段可以记录非加锁状态下的MarkWord,其中自然可以存储原来的哈希码。重量级锁的哈希码应该是在堆中存储的
- 对于轻量级锁同样也是,MarkWord中存储了栈中锁的记录指针,而栈中的这个锁记录空间,会存储锁对象的MarkWord拷贝,该拷贝中可以包含indentity hash code,所以轻量级锁可以合identity hash code共存,哈希码和GC年龄自然也保存在这里,释放锁后会将这些信息写回到对象头
5、锁的粗化与消除
锁的粗化是应为在一个方法中两者锁锁块,底层的编译器为了避免锁的连续释放获得,所以将连着的代码块放在一个锁中,达到一个优化的目的。锁的消除就是所对象在方法的内部他不可能逃脱当前的线程而被其他线程使用,说白了这样的锁是没有任何意义的,所以说底层出于优化的考虑就将这个去除掉。
的代码块放在一个锁中,达到一个优化的目的。锁的消除就是所对象在方法的内部他不可能逃脱当前的线程而被其他线程使用,说白了这样的锁是没有任何意义的,所以说底层出于优化的考虑就将这个去除掉。