64位对象头

  1. |--------------------------------------------------------------------------------------------------------------|
  2. | Object Header (128 bits) |
  3. |--------------------------------------------------------------------------------------------------------------|
  4. | Mark Word (64 bits) | Klass Word (64 bits) |
  5. |--------------------------------------------------------------------------------------------------------------|
  6. | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁
  7. |----------------------------------------------------------------------|--------|------------------------------|
  8. | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁
  9. |----------------------------------------------------------------------|--------|------------------------------|
  10. | ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁
  11. |----------------------------------------------------------------------|--------|------------------------------|
  12. | ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁
  13. |----------------------------------------------------------------------|--------|------------------------------|
  14. | | lock:2 | OOP to metadata object | GC
  15. |--------------------------------------------------------------------------------------------------------------|

image.png

  • 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函数

升级为偏向锁

  1. 默认开启延迟偏向锁,添加jvm参数 关闭延迟偏向锁

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

  1. jvm启动运行默认超过4s后,对象开启偏向锁
  2. 计算了hashcode就不能偏向了

    偏向锁的CAS

    是拿同步对象的mark_word值和无锁状态下的mark_word值比较,

  3. 偏向锁CAS成功,就将当前线程id写入到markword上,此时是依然是偏向锁。

  4. 偏向锁CAS失败
    1. 正常情况,线程会在栈帧中开辟一个锁记录,该对象偏向撤销变为无锁状态,然后升级为轻量级锁。
    2. 当对象偏向撤销,该对象的class中epoch值会+1,当达到阈值20,就会发生批量重定向,依然为偏向锁,将当前线程id写入到对象的markword上。
    3. 当epoch值达到40,又有其他线程同步该对象,那么就会发生批量撤销,那么就会升级为轻量级锁。

      轻量级锁

      当一个线程结束同步,另一个线程也同步同样的对象这时候对象就变成轻量级锁。
      如果偏向锁要想CAS变为轻量级锁,首先要将偏向锁撤销变为无锁状态,
      轻量级锁退出同步块之后,会还原对象头为无锁,不会包含线程id。
      image.png

重量级锁

调用到了os加锁函数
调用wait()就变成了重量级锁

证明偏向锁

前提偏向锁,只调用一次os函数

  1. 修改os上锁的函数,加上打印该 操作系统的线程id

    修改os函数, pthread_mutex_lock() //上锁
    // 只要调用了 os上锁函数 , 就会打印线程
    pthread_mutex_lock(){
     fprint(stdout,"msg"+pthread_self); // std控制台,显示器,输出线程id
    }
    
  2. 在我们的加锁的方法上,打印系统的线程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();
    }
    
  3. 证明两种情况

    1. 开启2个线程, 会多次一起出现打印 os上锁函数线程idjava打印的线程id
    2. 开启1个线程, 只会出现一次打印 os上锁函数线程idjava打印的线程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

注: 以上获取头信息可以参考以下流程

  1. 使用maven的方式,添加jol依赖
    <dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.8</version>
    </dependency>
    
    使用jol工具类输出A对象的对象头
public static void main(String[] args){
    A a = new A();
    System.out.println(ClassLayout.parseInstance(a).toPrintable());
}

看看输出结果
synchronize锁原理 - 图3
输出的第一行内容和锁状态内容对应
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 ),如果没有获取锁,就会发生锁膨胀。

变为重量级锁

调用wait(),会将锁变为重量级锁

偏向锁特殊情况

  1. 正常情况,当一个线程持有的偏向锁做同步,然后当线程1执行完,接着另一个线程没有竞争的情况下,偏向锁会变为轻量级锁。
  2. 有一种情况,启动多个线程,但是每个线程之间,都是等上个线程死亡(join()或者sleep),可能会出现大家同步的对象还是偏向锁。(这是可能是系统内核线程或者jvm,后面的线程和前面的线程id是同一个,导致第二次判断是同一个线程。)

    CAS原理

    CAS有三个参数,第一个参数是指针(原来的值),第二参数是预期值,第三个参数是新值。
    因为CAS在操作系统中就是一条指令,不会被其他线程打断。一旦CAS操作成功就会将新值赋值给指针指向的位置。

    notify 和 notifyAll区别

    image.png
    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");
        }
    }
}
  1. 首先 javac SynchronizeDemo.java 变成 class
  2. 然后反编译查看 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同步机制

  3. 首先每个类都由Objec派生出,当线程发生同步,会去尝试获取对象的monitor,如果没有获得就会进入entryList中。

  4. 获取了monitor线程进入同步块,虚拟机就会设置 monitorenter进入同步块,退出同步就会设置为monitorexit,为了防止同步中出现异常,设置了第二个monitorexit。
  5. 当遇到wait就将同步的线程放入waitSet中,当使用notify就随机从waitSet取需要相同对象的线程放到entryList中,等待notify去唤醒。
  6. 当对象调用notify,就会随机从waitSet取一个线程,放到entryList中,然后线程去竞争monitor。
  7. 当对象调用notifyAll,就会从waitSet将持有该对象的所有的线程,放到entryList中,然后线程去竞争monitor。