Java教程

Java内置锁的核心原理(一)

本文主要是介绍Java内置锁的核心原理(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

引言:线程安全问题

什么是线程安全问题?当多个线程并发的访问一个Java对象时,无论系统如何调度这些线程,这个对象都能表现出一致的、正确的行为,那么我们就说对这个对象的操作是线程安全的。反之,对这个线程的操作不是线程安全的,发生了线程安全问题。

本文将回答如下几个问题:synchronize是如何保证线程安全的? synchronize加锁到底是怎么加的? 锁信息放在Object对象的什么位置? 如何查看锁升级的具体过程?等等。

关于synchronize的使用场景、synchronize与ReentryLock有什么区别将在下一期介绍Lock的博客中给出。

Java内置锁的核心原理

Java内置锁是一种互斥(独占)锁。当线程A占有一个对象的锁,线程B也想要尝试获取这个对象的内置锁时,线程B会等待或阻塞,直到线程A执行完成后后释放锁(抛出异常也会释放),如果线程A不释放,线程B将会永远等待下去。

Java中的每个对象都已可用作内置锁。线程进入同步代码块时会自动获取改锁,在退出代码块时会自动释放该锁。

1. synchronized关键字

synchronize关键字的使用方式有三种:

1.修饰一个普通方法

public synchronized void add1() {
   val ++;
}

此时仅有一个线程可进入这个对象的这个同步方法。这时候锁是加在这个对象上的,即这个对象的this属性。

2.修饰代码块

public void add3() {
   synchronized (this) {
      val ++;
   }
}

与上面的代码稍有不同,synchronize修饰的是一段代码,因为很多情况下并不需要将整个方法全部锁住,仅锁住部分代码即可保证线程安全的执行。

还有一点不同的是我们既可以使用synchronize包裹住this对象,也可以包裹住其他的对象,实现更为灵活的锁操作。

3.修饰一个静态方法

public static synchronized void add2() {
   val ++;
}

Java的对象可以分为两大类,一类是Object对象,分配在JVM的堆中,另一类是Class对象,存放在方法区(Java1.8 的HotSpot实现中称为元空间),JVM中一个Class文件只会有一个Class类,而由Class实例化出的Object对象会有很多个。synchronize修饰静态方法时便是将锁加在了Class对象中。

小结:以上三种加锁的方法各异,但本质类似,都是锁住了一个Java对象从而实现一次只有一个线程可以访问同步代码块。好,了解了原理,那我们就来看看他的实现,synchronize具体是实现如何锁住一个对象的?

2. Java对象结构与内置锁

在介绍内置锁之前,有必要先和大家介绍下Java对象的结构。

image-20211114163922614

一个Java对象可以分为三个部分:

1)对象头

对象头又包含三个字段:第一个是Mark Word,用来存储对象的GC信息,锁信息,hashcode值等。第二个是Class Pointer,存放的是类指针,虚拟机通过这个类指针这个对象是哪个类的实例。第三个是Array Length,是一个可选字段,当此对象为数组时才会存在,用于记录数组长度的数据。

2)对象体

这部分包含对象的实例变量,包含父类的属性,这部分按照4字节对齐。

3)对齐字节

也叫填充区,其作用是保证这个对象占用的内存字节数为8的整数倍,因为对象头是8位的,所以仅需保证对象体也是8的倍数即可,当对象的实例变量数据不为8的倍数时,便需要填充来保证8字节的对齐。

Mark Word、Class Pointer、Array Length等字段的长度都与JVM的位数有关。Mark Word的长度为JVM的一个Word(字)大小,也就是说32位JVM的Mark Word为32位,64位JVM的Mark Word为64位。Class Pointer(类对象指针)字段的长度也为JVM的一个Word(字)大小,即32位JVM的Mark Word为32位,64位JVM的Mark Word为64位。所以,在32位JVM虚拟机中,Mark Word和Class Pointer这两部分都是32位的;在64位JVM虚拟机中,Mark Word和ClassPointer这两部分都是64位的。

不同锁状态下32位Mark Word的结构信息

image-2021111417134723864位的Mark Word与32位的Mark Word结构相似,

不同锁状态下64位Mark Work的结构信息

image-20211114171406229

对象加内置锁及锁的升级过程

无锁 --> 偏向锁:

当一个线程进入同步代码块时,发现此对象没有线程占用,那么这个对象就使用CAS(null, threadID)null是期望的值,threadID是将要写入的值,将自己的线程ID写入对象的Mark Word中,如果写入成功,即对象中没有threadID,则对象由原来的无锁状态变为偏向锁状态,lock不变化,为01,将偏向锁的标志位biased发生变化,由0变为1。

偏向锁 --> 轻量级锁:

当一个线程进入同步代码块时,使用CAS(null, threadID)将自己的线程ID写入对象的Mark Word中,如果写入失败,则说明此时线程已经被占用,则撤销对象的偏向锁定状态,升级为轻量级锁。对象Mark Word中的lock位由01变为00;

轻量级锁 --> 重量级锁:

轻量级锁的本意是为了减少多线程进入操作系统底层的互斥锁(Mutex Lock)的概率,并不是要替代操作系统互斥锁。所以,在争用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的重量级锁。轻量级锁升级为重量级锁的条件较为复杂,我们下一讲在详细探究。

这篇关于Java内置锁的核心原理(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!