概念:
自旋锁:是指线程在获取锁的时候,如果其他线程已经占用,则等待,循环判断是否可以获得锁,直到获得了锁才会退出循环。
悲观锁:悲观锁假设当前一定会出现并发问题,因此先锁定资源,其他线程无法占用,知道当前线程解锁。
乐观锁:乐观锁假设当前不会出现并发问题,先使用资源,更新资源的时候如果发现被其他线程改过则重新获取。乐观锁其实就是CAS。
共享锁:又叫读锁,所有线程都可以加读锁,就是不能加写锁。
独占锁:又叫写锁,只有当前线程可以读写,其他线程不能读写。
可重入锁:当前线程可以重复进入同一把锁同步的其他代码,与其他线程互斥。本质就是可以多次上锁。可重入锁使用的场景例如递归调用、一个事务中需要调用多个方法,多次方法等。synchronized锁属于可重入锁。
不可重入锁:当前线程不能重复进入同一把锁同步的其他代码。
公平锁、非公平锁:是根据锁争夺规则,遵守先来后到就是公平锁,否则就是非公平锁。
对象锁、类锁、分布式锁:这3种锁是根据作用范围来区分的,分布式是锁针对多个jvm进程的锁。
java中加锁的方式:
主要有2种,一个是使用synchronized关键字,另一种是使用J.U.C包中的lock实现类。前者是jvm底层支持,后者是jdk实现的。
synchronized关键字:
synchronized是可重入锁、独享锁、悲观锁。
修饰实例方法、静态方法时,隐式指定锁对象。
修饰代码块时,显式指定锁对象。
synchronized关键字的原理:
- jvm内存结构:
主要有方法区、堆、栈,首先类会被加载到方法区,然后根据类在堆中创建对象实例。
栈内存着对象的引用,对象头信息存放了类的引用,类中有关于父类子类的引用。
对象的结构具体分为对象头、对象体、padding,对象头记录对象的运行时相关的信息,比如对象类型(class的引用),是否要被GC,锁状态等,所谓加锁就是修改对象头中的锁相关的属性。
- 对象头的结构:
对象头主要分Mark Word、Klass Pointer、Array length,根据32位系统和64位系统,这几部分站位都为32位和64位。
Array length:这部分不是必须的,只有数组才有这部分,用于记录数组长度。通常jvm是可以直接通过对象的元数据获取对象的大小(也就是说类在定义的时候占用空间大小就是可以算出来的),但是数组这种结构不行,所以需要Array length来记录。
Klass Pointer:这部分主要存放类元数据的引用,可以知道当前对象是那种类型。
Mark Word:这里主要存放对象运行时相关的信息,如hashCode, GC标识,分代年龄、是否是偏向锁、锁状态等。
根据当前锁状态,Mark Word的内存结构是可以被改变的。
- 锁升级的过程
由于线程的竞争,对象的锁状态会经历“无锁”->“偏向锁”->“轻量级锁”->“重量级锁”的过程。
锁的升级过程是单向的,不会降级。
1)一个普通的对象如果没被设置为锁的时候,锁标志位=01,是否偏向锁=0
2)当一个对象被设置为同步锁(synchronized),并且有一个线程A抢到的时候,锁标志位=01,是否偏向锁=1,同时Mark Word会记录当前线程A
的id,这就是偏向锁。
3)如果这个线程A再次尝试获取此对象的锁,因为当前对象已经是偏向锁并且记录的线程id没变,所以线程A可以再次执行锁内的代码。
4)如果此时线程B尝试获取此对象的锁,就会出现锁竞争,就是线程B会通过CAS的方式不停的尝试获取锁一定次数(自旋),这个次数是jvm自己控制的,如果成功,则Mark Word记录的线程id变为B的线程id,由B执行锁内的代码。
5)如果B争抢不到,则对象的锁会升级为轻量级锁。此时jvm会在当前线程A的栈内开辟一个单独的空间,用来保存当前对象的Mark Word地址,同时Mark Word 中也会保存A栈中这块空间的地址,当前对象的锁标志位变成00,存储结构也随之变化。轻量级锁互相引用的过程同样也遵循着CAS的方式,也就是说只要A给B机会,对象随时都可以易主。
6)如果线程B还是争抢不到锁,则对象会继续升级为重量级锁。重量级锁的处理方式不再是“CAS+自旋”,而是直接阻塞未抢到锁的线程。
7)重量级锁执行流程:
首先重量级锁中线程的管理交给了一个特有的对象,叫Monitor(对象监视器)。
Monitor有几个主要结构:
- Owner 用来记录当前占有锁的线程。Owner的占有和释放的过程中Monitor还会有一个+1计数的行为,用来处理当前线程重复锁的情况,即重复锁了几次就要释放几次才算释放。
- EntryList 等待线程队列,这个队列遵循先来后到,如果Owner指向为null,Monitor会取这个队列尾部的那个线程让他参与Owner竞争(还是CAS方式),如果竞争失败则还得放回队列头部。
- WaitSet 当线程被程序挂起的时候,会进入这个队列,直到被唤醒才会参与Owner的竞争(CAS方式),如果竞争失败,会被放到EntryList排队。
首先线程会通过CAS争抢Owner,争到的线程Owner会记录这个线程的地址,计数+1,其他没有争到的线程会被安排到EntryList排队等待,执行过程中如果程序主动挂起了线程,则当前线程退出Owner占有权,计数 -1,放到WaitSet中等待唤醒,被唤醒后重新参与竞争。
虽然EntryList是有序的,但是没进入EntryList的线程依然都是通过CAS方式参与Owner竞争的,所以重量级锁并不是公平锁。
关于偏向锁的竞争,需要在合适的时间点,我们称之为“安全点”,在这个安全点发生的事情:
1)当前时间片没有正在执行字节码
2)检查当前线程是否存活,存活则会把自己设置为无锁状态,让所有线程竞争;不存活,则直接偏向最新的线程
关于读写锁 ReadWriteLock:
-
最重要的一点是读锁和写锁是互斥的,有线程上了读锁,就不能再备上写锁,反之亦然。
-
读锁可以多个线程同时上锁,互不影响。
可重入锁参考内容:https://www.zhihu.com/question/23284564
对象内存结构参考内容:
https://www.dazhuanlan.com/2020/02/02/5e364cd8d6996/
https://www.jianshu.com/p/3d38cba67f8b
https://blog.csdn.net/lengxiao1993/article/details/81568130
关于锁的升级过程细节:
https://blog.csdn.net/lengxiao1993/article/details/81568130
https://blog.51cto.com/14440216/2427707
网友评论