什么是线程安全问题?当多个线程并发的访问一个Java对象时,无论系统如何调度这些线程,这个对象都能表现出一致的、正确的行为,那么我们就说对这个对象的操作是线程安全的。反之,对这个线程的操作不是线程安全的,发生了线程安全问题。
本文将回答如下几个问题:synchronize是如何保证线程安全的?
synchronize加锁到底是怎么加的?
锁信息放在Object对象的什么位置?
如何查看锁升级的具体过程?
等等。
关于synchronize的使用场景、synchronize与ReentryLock有什么区别将在下一期介绍Lock的博客中给出。
Java内置锁是一种互斥(独占)锁。当线程A占有一个对象的锁,线程B也想要尝试获取这个对象的内置锁时,线程B会等待或阻塞,直到线程A执行完成后后释放锁(抛出异常也会释放),如果线程A不释放,线程B将会永远等待下去。
Java中的每个对象都已可用作内置锁。线程进入同步代码块时会自动获取改锁,在退出代码块时会自动释放该锁。
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具体是实现如何锁住一个对象的?
在介绍内置锁之前,有必要先和大家介绍下Java对象的结构。
一个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的结构信息
64位的Mark Word与32位的Mark Word结构相似,
不同锁状态下64位Mark Work的结构信息
无锁 --> 偏向锁:
当一个线程进入同步代码块时,发现此对象没有线程占用,那么这个对象就使用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)的概率,并不是要替代操作系统互斥锁。所以,在争用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的重量级锁。轻量级锁升级为重量级锁的条件较为复杂,我们下一讲在详细探究。