4 Java锁
目录
大厂面试题
一、Synchronized相关问题 1.Synchronized用过吗, 其原理是什么? 2.你刚才提到获取对象的锁,这个锁到底是什么?如何确定对象的锁? 3.什么是可重入性,为什么说Synchronized是可重入锁? 4.JVM对Java的原生锁做了哪些优化? 5.为什么说Synchronized是非公平 锁? 6.什么是锁消除和锁粗化? 7.为什么说Synchronized是个悲观锁?乐观锁的实现原理又是什么?什么是CAS, 它有 8.乐观锁一定就是好的吗?
二、可重入锁Reentrant Lock及其他显式锁相关问题 1.跟Synchronized相比,可重入锁Reentrant Lock其实现原理有什么不同? 2.那么请谈谈AQS框架是怎么回事儿? 3.请尽可能详尽地对比下Synchronized和Reentrant Lock的异同。 4.Reentrant Lock是如何实现可重入性的?
1, 你怎么理解iava多线程的?怎么处理并发?线程池有那几个核心参数? 2, Java加锁有哪几种锁?我先说了synchronized, 刚讲到偏向锁, 他就不让我讲了, 3, 简单说说lock? 4, hashmap的实现原理?hash冲突怎么解决?为什么使用红黑树? 5, spring里面都使用了那些设计模式?循环依赖怎么解决? 6,项目中那个地方用了countdown lan ch, 怎么使用的?
乐观锁和悲观锁
悲观锁
public synchronized void m1()
{
}
ReentrantLock lock = new ReentrantLock();
public void m2() {
lock.lock();
try {
}finally {
lock.unlock();
}
}
乐观锁
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
从8种情况演示锁的案例,看看我们到底锁的是什么
8锁案例
8锁演示
-
8锁案例 class Phone{
public static synchronized void sendEmail(){
try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("-------------sendEmail");
}
public synchronized void sendSMS(){
System.out.println("-------------sendSMS");
}
public void hello(){
System.out.println("-------------hello");
}
}
public class lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
phone.sendSMS();
},"b").start();
}
}
8锁原理
8锁-3个体现
字节码角度分析synchronized实现
文件反编译技巧
synchronized同步代码块
public class LockSyncDemo {
Object object = new Object();
public void m1(){
synchronized (object){
System.out.println("-----hello synchronized code block");
}
}
public static void main(String[] args) {
}
}
- 从target中找到LockSyncDemo.class文件,右键,open in terminal,然后
javap -c LockSyncDemo.class
public class com.zhang.admin.controller.LockSyncDemo {
java.lang.Object object;
public com.zhang.admin.controller.LockSyncDemo();
Code:
0: aload_0
1: invokespecial #1
4: aload_0
5: new #2
8: dup
9: invokespecial #1
12: putfield #3
15: return
public void m1();
Code:
0: aload_0
1: getfield #3
4: dup
5: astore_1
6: monitorenter
7: getstatic #4
10: ldc #5
12: invokevirtual #6
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any
public static void main(java.lang.String[]);
Code:
0: return
}
synchronized普通同步方法
public class LockSyncDemo {
public synchronized void m2(){
System.out.println("------hello synchronized m2");
}
public static void main(String[] args) {
}
}
- 类似于上述操作,最后调用
javap -v LockSyncDemo.class
.....
public synchronized void m2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/zhang/admin/controller/LockSyncDemo;
......
-
总结
- 调用指令将会检查方法的****访问标志是否被设置。如果设置了,执行线程会将先持有monitore然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
synchronized静态同步方法
public class LockSyncDemo {
public synchronized void m2(){
System.out.println("------hello synchronized m2");
}
public static synchronized void m3(){
System.out.println("------hello synchronized m3---static");
}
public static void main(String[] args) {
}
}
......
public static synchronized void m3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #2
3: ldc #5
5: invokevirtual #4
8: return
LineNumberTable:
line 15: 0
line 16: 8
......
-
总结
- ******, ******访问标志区分该方法是否是静态同步方法。
反编译synchronized锁的是什么
概念-管程
-
管程概念
-
管程:Monitor(监视器),也就是我们平时说的锁。监视器锁 -
信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。 -
执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
大厂:为什么任何一个对象都可以成为一个锁?
ObjectMonitor.cpp 中引入了头文件(include)objectMonitor.hpp
140行
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
- 追溯底层可以发现每个对象天生都带着一个对象监视器。
提前熟悉锁升级
synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位
公平锁和非公平锁
ReentrantLock抢票案例
class Ticket
{
private int number = 30;
ReentrantLock lock = new ReentrantLock();
public void sale()
{
lock.lock();
try
{
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class SaleTicketDemo
{
public static void main(String[] args)
{
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"a").start();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"b").start();
new Thread(() -> { for (int i = 0; i <35; i++) ticket.sale(); },"c").start();
}
}
非公平锁
公平锁
-
ReentrantLock lock = new ReentrantLock(true); -
买卖票一开始a占优,后面a b c a b c a b c均匀分布 -
是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。
为什么会有公平锁/非公平锁的设计?为什么默认是非公平?
-
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。 -
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平?什么时候用非公平?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了; 否则那就用公平锁,大家公平使用。
AQS提前了解
可重入锁(又名递归锁)
可重入锁说明
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取****锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
“可重入锁”详细解释
可重入锁种类
隐式锁Synchronized
Synchronized的重入实现机理
140行
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
-
ObjectMoitor.hpp 底层:每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。_count _owner -
首次加锁:当执行monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1 。 -
重入:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1 ,否则需要等待,直至持有线程释放该锁。 -
释放锁:当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
显式锁Lock
- 显式锁(即Lock)也有ReentrantLock这样的可重入锁
感觉所谓的显式隐式即是指显示/隐式的调用锁
public class ReEntryLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
{
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();
}
}
}
- 假如
lock unlock 不成对,单线程情况下问题不大,但多线程下出问题
public class ReEntryLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
} finally {
lock.unlock();
}
} finally {
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println("t2 ----外层调用lock");
}finally {
lock.unlock();
}
},"t2").start();
}
}
死锁及排查
死锁是什么
-
死锁
-
死锁产生的原因
-
系统资源不足 -
进程运行推进的顺序不合适 -
资源分配不当
请写一个死锁代码case
public class DeadLockDemo {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
new Thread(()->{
synchronized (object1){
System.out.println(Thread.currentThread().getName()+"\t 持有a锁,想获得b锁");
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
synchronized (object2){
System.out.println(Thread.currentThread().getName()+"\t 成功获得b锁");
}
}
},"A").start();
new Thread(()->{
synchronized (object2){
System.out.println(Thread.currentThread().getName()+"\t 持有b锁,想获得a锁");
synchronized (object1){
System.out.println(Thread.currentThread().getName()+"\t 成功获得a锁");
}
}
},"B").start();
}
}
如何排查死锁
纯命令
-
jps -l 查看当前进程运行状况 -
jstack 进程编号 查看该进程信息
图形化
win + r 输入jconsole ,打开图形化工具,打开线程 ,点击 检测死锁 。
小总结-重要
- 指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp,C++实现的)
以下相当于一些前置知识,为后面的章节做铺垫
写锁(独占锁)/读锁(共享锁)
自旋锁SpinLock
无锁-独占锁-读写锁-邮戳锁
无锁-偏向锁-轻量锁-重量锁
|