【视频笔记】面试官内心os:坏了今天碰到了个锁王!!!

L1、L2级缓存时cpu私有的,L3缓存时共享的,cpu从内存读取数据到缓存中(线程A),然后修改了缓存,当缓存还没有同步到主存时,另一个线程B从主存中读取数据,读到的就是旧数据,这就是可见性问题。
执行代码的时候,cpu或者编译器可能会对指令进行重排序,导致执行顺序和代码书写顺序不一致,这就是有序性问题
synchronized能保证一个线程拿到锁执行的一段时间内,其他线程不能执行同一段代码,这就保证了原子性

synchronized的编译之后就是monitor enter和monitor exit两个指令
monitor enter就是加锁,加锁的时候会使用读屏障,强行去从主存读取数据,也就是说它能保证读到的数据都是最新的
monitor exit是就是解锁,解锁的时候通过写屏障,保证强制将cpu缓存中的变量刷新到主存中,能够保证线程修改的数据,对其他线程立刻可见,这就保证了可见性
synchronized能够通过内存屏障防止指令重排,这就是有序性
synchronized修复普通方法,锁的就是this,就是当前对象,也就是说一个对象用一把锁
修饰静态方法,锁的事类.class对象,也就是类的所有对象共用一把锁
修饰代码块,括号里写的什么,它就锁什么

synchronized在jdk1.6做了锁升级优化,为什么要做锁升级呢,不做锁升级就慢,
synchronized在加锁时调用底层mutex,然后又涉及线程的阻塞与唤醒,每一个java线程对应一个操作系统的内核级线程,
每次切换线程都需要操作系统从用户态切换到内核态,开销很大。优化:在低并发情况,锁竞争比较少的情况下,就不让你阻塞;并发量比较高,锁竞争激烈时,再让你去阻塞,这就有了锁升级的过程,从无锁到偏向锁到轻量级锁到重量级锁。

一个系统大多数时候都是不存在锁竞争的,高并发的时候往往发生在一些特定时间,为了在低并发的时候提高性能,所以做了锁升级
锁升级的过程,其实是为了应对越来越激烈的锁竞争的过程。

第一个线程来时,jvm将对象头的Mark word锁标志位设置为偏向锁,然后将线程ID记录到那个Mark word里面,这个时候,这个线程进入同步代码块
就不需要其他的同步操作了,非常的轻,非常的快。那为什么要有偏向锁呢?偏向锁考虑的那种只有一个线程抢锁的场景。
那什么时候升级到轻量级锁呢?当第二个线程来抢锁,就升级为轻量级锁。第二个线程抢不到锁,就采用cas+自旋的方式,不断重新尝试获取锁。
为什么要有轻量级锁呢,轻量级锁考虑的是锁竞争的线程不多,而且线程持有锁的时间也不长的一个情景,因为阻塞线程需要操作系统从用户态切换到内核态,代价比较大,如果刚刚才阻塞了这个线程然后这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程了,让它自旋等待锁释放。

那自旋的性能一定要比阻塞的性能好吗?自旋是让线程一直循环执行抢锁命令,线程是一直在运行的,它没有被阻塞,所以就减少了线程上下文切换的一个开销,操作系统就不要切换到内核态了。短时间的自旋性能是不错的,但是长时间的自旋会让CPU一直在空转,cpu没有办法执行其他任务,会浪费cpu,所以这个轻量级锁,只放在了两个线程竞争的场景下使用,而且这里的自旋还是适应性自旋,自旋的时间由上一个自旋的时间去决定。
可以看到偏向锁它只记录了线程ID,而轻量级锁只是自旋抢锁而已,他们都没有做操作系统级的线程阻塞和切换,不需要操作系统切换到内核态去做操作,整个过程只在用户态就可以完成了。
所以在较低的锁竞争的时候,偏向锁和轻量级锁的设计就提高了性能。

那轻量级锁什么升级为重量级锁呢?当第二个线程自旋到一定次数之后,还是没拿到锁,获取锁失败了,或者当有更多的线程来抢锁了,那就升级为重量级锁。
重量级锁加锁,就需要调用操作系统的底层原语mutex,所以每次切换线程都需要操作系统切换到内核态,开销很大,这就是为什么称为重量级锁。
轻量级锁不升级行吗,如果大量的线程自旋,很浪费cpu,当某个线程执行的时间非常长,那自旋的时间就会非常长,长时间的自旋也是很浪费cpu的
所以升级到重量级锁,把这些没拿到锁的线程都给阻塞住。
当升级到重量级锁时,对象头的Mark word的指针就会指向锁监视器monitor。那为什么要有锁监视器呢?
锁监视器主要是用来负责记录锁的拥有者,记录锁的重入次数,负责线程的阻塞唤醒,准确来说,锁监视器就是一个对象,他有这么几个字段:

 首先是owner,用来保存持有锁的线程;然后重入次数的一个计数器,用来记录锁的重入次数;然后还要锁池和等待池。

锁池用来管理抢锁失败的线程,等待池用来管理调用wait()方法陷入等待状态的一个线程

synchronized的可重入是怎么实现的,就是依赖这个重入次数计数器,重入一次计数器加一次,释放一次计数器减一次,减到零,就完全释放锁了。
在重量级锁状态,当有线程拿到锁,此时监视器的owner字段就记录拿到锁的线程,没有拿到锁的线程就被阻塞住,进入blocking状态,然后放到锁池中
当拿到锁的线程调用了wait方法,那该线程就释放锁,然后进入waiting状态,然后被放到等待池当中。
当某个线程调用了notify唤醒了这个waiting的线程,那这个线程就从waiting的状态变成blocking状态,然后再被放入到锁池中,等待锁的释放,重新去抢锁。这就是锁池和等待池的作用。

锁竞争失败的线程和调用了wait方法等待的线程有什么本质区别吗?为什么要放到锁池和等待池这两个完全不同的集合中呢?
首先锁池存放的是竞争锁失败的线程,线程状态是blocking,竞争锁失败,那它的目标就是尽快获取锁去执行任务,这是锁的互斥问题
等待池放的是主动放弃锁的线程,主动放弃锁,暂时还不需要锁,这个线程等待被其他线程唤醒之后,去配合其他线程去完成某项任务的,线程状态是waiting或者time waiting,这些waiting状态的线程,等某个资源到位了,等某个事情完成了,然后再被notify唤醒,然后放入锁池中,准备去抢锁做业务,这是线程通信问题。
所以等待池的线程和锁池的线程,他的目标和要解决的问题完全不一样,当然要放到两个不同的集合了

 

一些评论:

posted @ 2025-05-27 16:08  fanblog  阅读(12)  评论(0)    收藏  举报