在上一篇《如何保证线程的原子性》中,我们谈到了锁(Synchronized),
这次我们就来深入探讨一下Java多线程中的锁。
互斥锁的本质是共享资源。
如上图所示,
- Thread1访问受保护资源,对其加锁,将受保护资源的锁加成粉红色。
- 这时Thread2来访问,发现锁对象是红色的,就阻塞等待。
- 当Thread1 释放锁后,锁恢复为绿色,表示资源可以使用。
- Thread2 可以正常访问资源。
上面例子中的粉色、绿色,其实就是锁对象的一些属性,这些属性存储在哪里呢?
接下来我们就来说一下 锁的存储。
锁的存储,跟对象头有关系。
在hotspot 虚拟机中,对象头就是表示一个对象在内存中的布局。
比如我们new了一个lock对象,那么它在虚拟机中会构建一个对象布局。
我们从虚拟机源码中可以看出一些端倪。
在 instanceOop.hpp
文件中:
#ifndef SHARE_VM_OOPS_INSTANCEOOP_HPP
#define SHARE_VM_OOPS_INSTANCEOOP_HPP
#include "oops/oop.hpp"
// An instanceOop is an instance of a Java Class
// Evaluating "new HashTable()" will create an instanceOop.
class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
// If compressed, the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes() {
// offset computation code breaks if UseCompressedClassPointers
// only is true
return (UseCompressedOops && UseCompressedClassPointers) ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size) {
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes &&
(offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
}
};
#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP
每个Java的Class实例,都会对应一个这样的 instanceOop
来看一下它的父类 oopDesc
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
......
}
oopDesc
中的 markOop
就是我们上图中的对象头, _metadata
是元数据。
我们打开 markOop.hpp文件,可以看到对象头的每位所存储的内容的注释:
通过上述内容,可以知道,锁对象中有个空间来存储锁的状态。
当线程来临的时候,就可以通过锁状态来判断,是否可以进入到当前代码。
我们可以使用jol-core
这个工具来打印出来类的布局,加深理解。
代码pom.xml中加入:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
编写一个类,输出一个无锁状态下的打印:
import org.openjdk.jol.info.ClassLayout;
public class MyClassLayout {
public static void main(String[] args) {
MyClassLayout myClassLayout = new MyClassLayout();
System.out.println(ClassLayout.parseInstance(myClassLayout).toPrintable());
}
}
执行结果:
demo.MyClassLayout object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0
对象头的每位所存储的内容可以通过下表来看:
再对照上述打印出的类布局:
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
注意 这里是小端模式存储的,所以是下面的顺序。
16进制: 0x 00 00 00 00 00 00 00 01
(64位) 2进制:00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
对照上面表格,最后三位 0 01 就是表示无锁状态。
我们将代码中加入锁,再看下类布局:
public class MyClassLayout {
public static void main(String[] args) {
MyClassLayout myClassLayout = new MyClassLayout();
synchronized (myClassLayout) {
System.out.println(ClassLayout.parseInstance(myClassLayout).toPrintable());
}
}
}
执行结果:
demo.MyClassLayout object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) a8 f4 df e7 (10101000 11110100 11011111 11100111) (-404753240)
4 4 (object header) 5a 00 00 00 (01011010 00000000 00000000 00000000) (90)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
上述结果中,最后三位是:000
表示轻量级锁。
我们多个线程访问一个资源时,我们判断是谁抢占来锁,一定要做出标记,来记录锁的持有者。
加锁一定会带来性能开销。所以为了优化,设计了多种性能开销不同的锁。
对于这些锁,各自有什么特性,我们后续再讲。