其实在JDK 1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低的锁,会由jvm用户态切换到操作系统的管理来实现互斥:
管程 ( Monitor)
Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”,在java领域就是“对象锁”。
管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。翻译成Java领域的语言,就是管理类的状态变量,让这个类是线程安全的。
**synchronized关键字和wait()、notify()、notifyAll()**这三个方法是Java中实现管理技术的组成部分。
Monitor有两种大作用:同步和互斥
wait/notify基于monitor做的,monitor中有owner、entryList、waitSet
java对象与 Monitor之间的关系
“每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象加锁(重量级锁)之后,该对象头的 Mark Word 中就被设置指向 Monitor对象 的指针。
Monitor的基本结构
Owner字段: 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
EntryQ字段: 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程
RcThis字段: 表示blocked或waiting在该monitor record上的所有线程的个数
Nest字段: 用来实现重入锁的计数
HashCode字段: 保存从对象头拷贝过来的HashCode值(可能还包含GC age)
Candidate字段: 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待
的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降;Candidate只有两种可能的值0表示没有需要唤醒的
线程1表示要唤醒一个继任线程来竞争锁。
Monitor与java对象以及线程关联
如果一个java对象被某个线程锁住,则该java对象的Mark Word字段中LockWord指向monitor的起始地址
Monitor的Owner字段会存放拥有相关联对象锁的线程id
方法
Monitor的enter方法:获取锁
Monitor的exit方法:释放锁
Monitor的wait方法:为java的Object的wait方法提供支持
Monitor的notify方法:为java的Object的notify方法提供支持
Monitor的notifyAll方法:为java的Object的notifyAll方法提供支持
结构图如下:
刚开始 Monitor 中 Owner 为 null,当线程-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 线程-2, Monitor中只能有一个 Owner、在 线
程-2 上锁的过程中,如果 线程-3,线程-4,线程-5 也来执行 synchronized(obj),就会进入EntryList,此时线程状态变为BLOCKED状态、Thread-2 执行完同步
代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的(synchronized是非公平锁)和WaitSet 中的 线程-0,线程-1是之前获得过锁的线
程,此时的状态是 WAITING 状态,
wait-notify时的分析如下:
锁升级
JDK 1.6之后,为提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁和轻量级锁,从此以后Java内置锁的状态就有了4种(无锁、偏
向锁、轻量级锁和重量级锁),并且4种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)。
级别由低到高依次为: 无锁一>偏向锁一>轻量级锁一>重量级锁。
Mark Word 标记
主要作用是标识出当前对象的线程锁状态,GC状态标识。
age表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
并行GC次数,默认15次。
并发GC次数,默认6次。
age标识位,只有4位,最大能表示的数字:15 这也是 -XX:MaxTenuringThreshold 这个参数最大只能设置15的原因。
采用小工具查看
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
<scope>compile</scope>
</dependency>
示例:
User user = new User();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
System.out.println(ClassLayout.parseClass(User.class).toPrintable());
在无锁情况下,如图:
偏向锁
偏向锁存 jdk中偏向锁存在延迟4秒启动,也就是说在jvm启动后4秒后创建的对象才会开启偏向锁,可以通过jvm参数取消这个延迟时间。 创建的对象状态为
对象处于无锁状态+可偏向状态 偏向锁位(biased_lock)1bit + 锁标志位(lock)2bit = 1 01
在没有线程竞争的条件下,第一个获取锁的线程通过CAS将自己的threadId写入到该对象的mark word中,若后续该线程再次获取锁,需要比较当前线程
threadId和对象mark word中的threadId是否一致,如果一致那么可以直接获取,并且锁对象始终保持对该线程的偏向,也就是说偏向锁不会主动释放。
Thread.sleep(4100);//jvm启动后延迟4s以后
User user = new User();
synchronized (user){
System.out.println(ClassLayout.parseInstance(user).toPrintable());
Thread.sleep(4100);//jvm启动后延迟4s以后
User user = new User();
synchronized (user){
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
System.out.println(ClassLayout.parseInstance(user).toPrintable());
synchronized (user){
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
轻量级锁
当两个或以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋方式尝试获取锁,避免阻塞线程
造成的cpu在用户态和内核态间转换的消耗 主线程首先对user对象加锁,首次加锁为101偏向锁 子线程等待主线程释放锁后,对user对象加锁,这时将偏向锁升级
为00轻量级锁 轻量级锁解锁后,user对象无线程竞争,恢复为001无锁态,并且处于不可偏向状态。如果之后有线程再尝试获取user对象的锁,会直接加轻量级
锁,而不是偏向锁。
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100);//jvm启动后延迟4s以后
User user = new User();
synchronized (user){
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
Thread thread=new Thread() {
public void run() {
synchronized (user) {
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
}
};
thread.start();
thread.join();
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
重量级锁
两个或以上线程在同一个对象上进行同步时,为了避免无用自旋消耗cpu,轻量级锁会升级成重量级锁。这时mark word中的指针指向的是monitor对象(也
被称为管理或监视器锁)的起始地址
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100);//jvm启动后延迟4s以后
User user = new User();
synchronized (user){
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
Thread thread1=new Thread() {
public void run() {
synchronized (user) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
}
};
thread1.start();
Thread thread2=new Thread() {
public void run() {
synchronized (user) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
}
};
thread2.start();
thread1.join();
thread2.join();
TimeUnit.SECONDS.sleep(3);
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}