JUC锁-复习-自用
前几天复习了设计模式。里面用到了 synchronized 和 volatile。
今天终于有时间,下班的早,把这两个相关的知识给一起复习一下。
要求:用口头话语言表述。即便是对着念 也不给面试官一种自己是在背八股的感觉。
一、JUC锁
1 synchronized
面试官问:讲讲你对synchronized的了解?
我:好的。面试官。synchronized是java中的一个关键字,他主要用于修饰方法或者代码块,它能够确保这个方法或者代码块,同一时刻只能被一个线程给执行。同时它还有另一个重要的作用,就是可见性的保证。当一个线程对这个代码块或者方法里的共享变量进行修改的时候。这个修改是能够马上被其他线程给看到的。这叫可见性。是它的第二个重要的作用。
面试官:嗯。再讲讲synchronized的几种使用方式,不同方式下,都是锁的什么。
我:synchronized主要用于修饰 方法或者代码块。方法的话 又分为静态方法和实例方法。如果修饰的是静态方法的话,锁的是这个类的class对象;实例方法的话,锁的是这个类的实例对象。修饰代码块的话,锁的是括号里面的对象。如果是this,表示修饰的是当前实例对象。如果是xx.class,表示修饰的是当前类的class对象。
面试官:讲讲synchronized的底层实现原理,怎么实现同步的。
我:
当修饰的是代码块的时候,是通过monitorenter和monitorexit这两个指令来保证的。monitorenter指向代码块开始的位置,当开始的时候,会尝试获取对象监视器monitor的持有权,若锁计数器为0,表示可获取,那就获取,然后计数器加1.若锁计数器不为0,那要看锁现在的持有者 是不是当前线程,如果是,可重入,锁的计数器加一,再次获取这个锁。如果不是,那就不能,获取锁失败。这是代码块开始的时候,然后代码块结束的时候会执行monitorexit指令,锁计数器减1,当锁的计数器件为0的时候,表示锁可以释放。
当修饰的是方法的时候。则是用acc_synchronized这个标识,而不再显示的调用那两个指令。当方法开始的时候,虚拟机会识别出这个标识,然后自动的获取到锁。当方法结束的时候,则是会自动的释放锁。
面试官:synchronized是怎么保证原子性、可见性、有序性、可重入性的
我:
原子性:一个操作在并发执行时,不会被其他线程干扰,且执行过程中不会被线程切换打断。monitor锁。分修饰代码块 和 修饰方法两种情况。具体同上
可见性:可见性就是说一个线程对代码块或者方法体里面的共享变量的修改,要被其他线程给看到。通过读主内存的方式来实现的。加锁前,清空共享变量里的指,重新从主内存中获得。加锁后,其他线程无法读取主内存。锁释放前,必须要把共享变量的最新值写入主内存。
有序性:指的是执行结果的有序性 而不是防止指令重排的有序性。synchronized保证被修饰的方法 在同一时刻 只能被 一个线程执行,换句话说,代码这时候是单线程执行的。又因为 as-if-serial语义的存在,单线程执行的程序,可以保证最终执行结果是有序的。
可重入性:锁的计数器。同一线程,获得锁,加1,退出代码块/方法,减1,计数器为0,锁释放。
面试官:讲讲synchronized的 锁升级 锁优化
我:好的。面试官。
锁升级是指 无锁 到 偏向锁 到 轻量级锁 到 重量级锁 的一个过程。
首先是无锁。无锁就是没有获得锁的时候。然后是偏向锁,偏向锁就是说一个线程在第一次进入这个代码块或者方法体的时候,若这个锁目前未被获取且偏向锁未被禁止,那么这个线程就会成功获取到这个锁,并且设置偏向锁标识为1,表明这是个偏向锁的意思,同时会在对象头里的markword里存储当前线程的id。后面当有线程再想获得这个锁的时候,分两种情况,一种是重入,是该线程再次想获取这个锁,此时这个线程 进入 退出代码块/方法体的时候,就不需要再花费CAS操作 来加锁、解锁了,另一种是 别的线程来获取这个锁 如果之前那个线程不活跃 则是偏向锁的拥有权 转移一下,如果还活跃 则会撤销偏向锁 重置偏向锁标识, 把锁标志位设置为00 ,同时使用CAS操作来把对象头的 Mark Word 替换为指向锁记录的指针, 替换完之后,就升级为轻量级锁了 ,此时这个轻量级锁被新线程给拿着。后面 当有线程 再想获得这个锁的时候,还是会通过CAS 操作 来尝试 将对象头的 Mark Word 替换为指向锁记录的指针。如果之前那个线程不活跃了,那这个新线程会CAS成功,得到这个轻量级锁;如果 之前线程还活跃 那CAS会失败,但它不会说就不获取了,它是会使用自旋重复的获取这个锁,直到达到自旋的次数上限。达到自旋的次数上限后, 这个轻量级锁 就会膨胀为重量级锁。而那个一直自旋的线程会阻塞,等待之前线程执行完成后去唤醒自己,然后再去尝试获取这个重量级锁。这个重量级锁 就是我们常说的 monitor锁 无论什么时刻 都只能由一个线程 来执行这个代码块/方法体。那么这是锁升级的一个过程。
至于锁优化的话,主要是这几个方面:
首先是 轻量级锁,轻量级锁的使用场景是 多个线程 在不同的时间下 去竞争锁,有竞争,但是是在不同的时间下,所以其实没有锁竞争。它的优化的点在于,没有轻量级锁之前,用的都是重量级锁 保证 任何时刻都只能有一个线程去执行这个代码块/方法体。但是 它的性能太低了,它是防止两个不同线程在某一时间段的竞争的,但是大多数情况下,压根就没有那么多的 在同一时间段的竞争。可能 是有竞争 但未必是在同一时间段,他们是错开的 所以用重量级锁 去锁死 这个代码块/方法 ,不管什么时刻都不让其他线程进来 哪怕之前那个线程其实已经停了下来 已经有了上下文切换 都不如其他线程进来 这是不对的 太严格了 也太影响性能了。所以直接用重量级锁 不合适,于是有了轻量级锁,它适合 多个线程 在不同的时间下 去竞争锁的场景。那么这是第一个优化。
第二个优化是 偏向锁,如果说轻量级锁是针对 两个不同线程之间的。那偏向锁就是针对同一个线程的。当同一个线程 多次想要获得这个轻量级锁时,因为它是重入,它就不需要再执行CAS操作去置换那个对象头里的markword 去获得锁 释放锁了。相当于消除了同步语句,和 CAS 操作,极大地提高了程序的运行性能。
再然后就是 锁粗化 和 锁消除 ,锁粗话是指 如果 虚拟机 检测到一系列连续的锁操作 其实是在同一个单一线程中完成的,那么虚拟机 就会 把这多个连续的锁操作 合并为一个更大的锁操作,减少锁请求的次数。锁消除就是说,虚拟机的即时编译器,JIT,会在运行时进行代码分析,如果发现某些锁操作不可能被多个线程同时访问,不可能被多个线程同时访问 那就没必要加锁啊 那么这个锁操作就会被消除,减少不必要的同步开销。
二、Volatile关键字
volatile 保证 可见性、 禁止指令重排序(有序性)、 但不保证原子性。
怎么保证 可见性的:可见性就是说 一个线程对共享变量的修改 要被其他线程给看到。它是这样做的,当线程对 volatile 变量进行写操作时,Java内存模型 会在写入这个变量之后 插入一个写屏障指令,这个指令会强制将本地内存中的变量值刷新到或者说同步到主内存中。那之后,当有线程对 这个volatile 变量进行读操作时,Java内存模型 会再插入一个 读屏障指令,这个指令会强制让本地内存中的变量值失效,失效了 ,所以要重新从主内存中读取最新的值。所以读到的是最新的。所以保证了可见性。
怎么保证 禁止指令重排序的(有序性):
volatile 和 synchronized 的区别:
volatile 加在基本类型和对象上的区别:
单例模式的双重锁 去讲解volatile在其中的作用:
三、延申
JMM
多线程的几种实现方式
CAS
#面试##实习##大厂无回复,继续等待还是奔赴小厂##那些我实习了才知道的事##牛客创作赏金赛#