64位对象头
|--------------------------------------------------------------------------------------------------------------|
| Object Header (128 bits) |
|--------------------------------------------------------------------------------------------------------------|
| Mark Word (64 bits) | Klass Word (64 bits) |
|--------------------------------------------------------------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁
|----------------------------------------------------------------------|--------|------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁
|----------------------------------------------------------------------|--------|------------------------------|
| | lock:2 | OOP to metadata object | GC
|--------------------------------------------------------------------------------------------------------------|
- lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
- age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
- identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
- thread:持有偏向锁的线程ID。
- epoch:偏向时间戳。
- ptr_to_lock_record:轻量级锁,就会将同步对象lock_record和线程栈中锁记录(存放着1. 对象无锁状态的markword,2. Owner 指针指向锁对象)的指针相互指向。
- ptr_to_heavyweight_monitor:重量级锁,指向管程Monitor的指针。
锁的级别
偏向锁
我们的方法一定要保证线程安全,但是实际情况不一定有互斥
所以偏向锁是synchronize锁的对象如果没有资源竞争的情况下
偏向锁不会调用os函数实现的,第一次会调用os函数
升级为偏向锁
- 默认开启延迟偏向锁,添加jvm参数 关闭延迟偏向锁
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- jvm启动运行默认超过4s后,对象开启偏向锁
-
偏向锁的CAS
是拿同步对象的mark_word值和无锁状态下的mark_word值比较,
偏向锁CAS成功,就将当前线程id写入到markword上,此时是依然是偏向锁。
- 偏向锁CAS失败
重量级锁
调用到了os加锁函数
调用wait()就变成了重量级锁
证明偏向锁
前提偏向锁,只调用一次os函数
修改os上锁的函数,加上打印该 操作系统的线程id
修改os函数, pthread_mutex_lock() //上锁 // 只要调用了 os上锁函数 , 就会打印线程 pthread_mutex_lock(){ fprint(stdout,"msg"+pthread_self); // std控制台,显示器,输出线程id }
在我们的加锁的方法上,打印系统的线程id
2.1. 编写c文件
JNIEXPORT void JNICALL Java_类名_tid(JNIEnv *env, jobject c1){ printf("current tid:%lu---\n",pthread_self()); usleep(700); }
2.2 java方法定义native方法
public native void gettid(); public synchronize void sync(){ gettid(); } main方法开启线程调用. public static void main(String[] args){ // 线程1 new Thread(run() -> { while(true){ Sleep(700); sync(); } }).start(); // 线程2 new Thread(run() -> { while(true){ Sleep(700); sync(); } }).start(); }
证明两种情况
- 开启2个线程, 会多次一起出现打印 os上锁函数线程id和java打印的线程id
- 开启1个线程, 只会出现一次打印 os上锁函数线程id和java打印的线程id, 然后只有java打印的线程id
那么说明,当不存在竞争的时候,锁住的对象,出现的是偏向锁, 而不是重量级锁。
对象头结构
mark word
56bit hashcode 1bit 无用 4bit年龄 1bit是否偏向 2bit锁类型
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 11 重量级锁
0 10 gc标记
jvm----------------Ox64a294a6
after hash
com.snake.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 a6 94 a2 (00000001 10100110 10010100 10100010)
4 4 (object header) 64 00 00 00 (01100100 00000000 00000000 00000000)
可以看出 高1位是对应的锁信息 00000001
高2,3,4,5是对应的hashcode, 高2位对应的是code低1位, 10100110 ---> a6
偏向锁的状态
可偏向状态
00000101 00000000 00000000 00000000
00000000 00000000 00000000 00000000
偏向状态
(注意的是如果是hashcode,那么出去高一位字节,应该占4个字节,这里只占了3个字节(指的是线程id的值))
00000101 10100110 10010100 10100010
00000000 00000000 00000000 00000000
不可为偏向状态
有hashcode了,那么久不可为偏向
00000001 10100110 10010100 10100010
10010100 00000000 00000000 00000000
注: 以上获取头信息可以参考以下流程
- 使用maven的方式,添加jol依赖
使用jol工具类输出A对象的对象头<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.8</version> </dependency>
public static void main(String[] args){
A a = new A();
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
看看输出结果
输出的第一行内容和锁状态内容对应
unused:1 | age:4 | biased_lock:1 | lock:2
0 0000 0 01 代表A对象正处于无锁状态
第三行中表示的是被指针压缩为32位的klass pointer
第四行则是我们创建的A对象属性信息 1字节的boolean值
第五行则代表了对象的对齐字段 为了凑齐64位的对象,对齐字段占用了3个字节,24bit
偏向锁和轻量级锁
时间差很多,偏向锁性能高很多
锁膨胀
多个线程交替执行,当存在锁竞争,线程自旋一段时候(这个时间是jvm可以设置,主要调用操作系统函数 pthread+spinlock_t ),如果没有获取锁,就会发生锁膨胀。
变为重量级锁
偏向锁特殊情况
- 正常情况,当一个线程持有的偏向锁做同步,然后当线程1执行完,接着另一个线程没有竞争的情况下,偏向锁会变为轻量级锁。
- 有一种情况,启动多个线程,但是每个线程之间,都是等上个线程死亡(join()或者sleep),可能会出现大家同步的对象还是偏向锁。(这是可能是系统内核线程或者jvm,后面的线程和前面的线程id是同一个,导致第二次判断是同一个线程。)
CAS原理
CAS有三个参数,第一个参数是指针(原来的值),第二参数是预期值,第三个参数是新值。
因为CAS在操作系统中就是一条指令,不会被其他线程打断。一旦CAS操作成功就会将新值赋值给指针指向的位置。notify 和 notifyAll区别
notify 会从waitSet《等待队列》中随机拿取那一个线程放到entryList《阻塞队列》中
notifyAll 会将waitSet所有线程都放到entryList中,唤醒哪个不确定,因为不确定谁竞争到了monitor。
wait
wait 会将线程从entryList 放回到waitSet中。
用synchronize还是用lock
用两者都可以,synchronize拥有的功能,lock都能实现。lock还有一些更高级的功能。 但是一般使用synchronize就可以了,他语法方便,使用简单。lock必须主动释放锁,比较麻烦。除非用到了lock高级功能,那只能用lock。
使用synchronize
public class SynchronizeDemo {
static Object object;
public static void main(String[] args) {
object = new Object();
synchronized (object) {
System.out.println("aaa");
}
}
}
- 首先 javac SynchronizeDemo.java 变成 class
然后反编译查看 javap -v SynchronizeDemo.class
10: getstatic #3 // Field object:Ljava/lang/Object;<br /> 13: dup<br /> 14: astore_1<br /> ** 15: monitorenter // (这是进入同步的监视器)**<br /> 16: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;<br /> 19: ldc #5 // String aaa<br /> 21: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V<br /> 24: aload_1<br /> ** 25: monitorexit //(这是第一次退出同步的监视器)**<br /> 26: goto 34<br /> 29: astore_2<br /> 30: aload_1<br /> ** 31: monitorexit //(这是第二次退出同步的监视器,防止同步块出现异常)**
Synchronize同步机制
首先每个类都由Objec派生出,当线程发生同步,会去尝试获取对象的monitor,如果没有获得就会进入entryList中。
- 获取了monitor线程进入同步块,虚拟机就会设置 monitorenter进入同步块,退出同步就会设置为monitorexit,为了防止同步中出现异常,设置了第二个monitorexit。
- 当遇到wait就将同步的线程放入waitSet中,当使用notify就随机从waitSet取需要相同对象的线程放到entryList中,等待notify去唤醒。
- 当对象调用notify,就会随机从waitSet取一个线程,放到entryList中,然后线程去竞争monitor。
- 当对象调用notifyAll,就会从waitSet将持有该对象的所有的线程,放到entryList中,然后线程去竞争monitor。