并发编程(五):深入理解synchronized

Java对象头(64位虚拟机)

  • 整个对象头一共有128位,Mark Word有64位,Klass Word有64位,但是Klass Word因为指针压缩的原因被压缩为32位,使用对象头一共有96位,而且现在我们更需要关注的是前64位。至于Klass Word指向了方法区的模板类,字节码。
  • 对象头还不是一成不变的,就表格可以看出,对象的状态会改变对象头的数值,这里我们分为5个状态,分别是无锁(001)、偏向锁(101)、轻量锁(00)、重量锁(10)和被gc标记(11)的对象。
  • 并发编程(五):深入理解synchronized_第1张图片
    64位虚拟机对象头.png

对象头占据内存

  • 例如:int是4个字节,Integer对象的Mark Word8字节,Klass Word8个字节(指针压缩后4字节),存储占4字节,存储一个数需要占8(Mark Word)+4(Klass Word)+4(存储占4字节)=16字节
    验证new Integer(1)占据内存大小

        
            org.openjdk.jol
            jol-core
            0.9
        

@Slf4j(topic = "ants.TestIntegerSize")
public class TestIntegerSize {
    public static void main(String[] args) {
        Integer i = new Integer(1);
        log.debug(ClassLayout.parseInstance(i).toPrintable());
    }
}

# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
2020-04-12 17:45:45.202 [main] DEBUG ants.TestIntegerSize 16 - java.lang.Integer 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)                           ae 22 00 f8 (10101110 00100010 00000000 11111000) (-134208850)
     12     4    int Integer.value                             1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • 由上面控制台结果分析,new Integer(1)会占据16字节,而我们知道int i=1只会占四个字节。

Monitor(锁) 监视器|管程

  • 每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级锁)之后,该对象头的Mark Word就被设置指向Monitor对象指针。


    并发编程(五):深入理解synchronized_第2张图片
    线程-对象-Monitor关系.png
  1. synchronized(obj)使用对象锁(obj)和Monitor对象一一对应
  2. 刚开始Monitor中Owner为null
  3. 当Thread-2执行synchronized就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
  4. Thread在上锁过程中,如果Thread-3,thread-4,thread-5也来执行synchronized(obj),就会进入EntryList中进阻塞(BLOCKED)状态
  5. Thread-2执行完同步代码块内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的(synchronized没有提供公平锁,非公平是指如果Thread-2释放锁的时候来了Thread-6,那么Thread-3和Thread-6将同时有机会获得obj对象)
  6. 图中Thread-0,Therad-1是之前获得过锁,但是条件不满足进入WAITING状态的线程(跟wait-notify相关)

注意

  • synchronized必须是关联统一个对象时,才会出现上面结果
  • 不加synchronized不会关联monitor监视器,不会出现上面结果

synchronized原理

并发编程(五):深入理解synchronized_第3张图片
代码.png

并发编程(五):深入理解synchronized_第4张图片
代码中对应的字节码.png
  • dup:将lock复制一份
  • Exception table:检测一定范围,如果范围内出现异常做处理
  • 6-16-19-any,表示如果在6-6行出现异常,java会在19行开始执行,同样保存lock引用,将对象头重置,唤醒EntryList,让其他线程竞争锁,最后将处理不了的异常抛出去
  • 在字节码中可以看出,即使synchronized中出现异常,synchronized也会释放锁

不同锁

  • Monitor
  • 轻量级锁
  • 偏向锁
  • 批量重偏向(一个类的偏向锁撤销达到20阈值)
  • 一个类的偏向锁撤销达到40阈值,对象升级为轻量级锁

轻量级锁

  • 使用场景:如果一个对象虽然有多线程访问,单多线程访问的时间是错开的(没有线程竞争),name可以用轻量级锁来优化。
  • 语法:轻量级锁对使用这透明,仍然使用synchronized
    轻量级锁加锁、解锁流程
public class LightLock {
    static final Object object = new Object();

    public static void method1(){
        synchronized (object){
            // 同步代码块1
            method2();
        }
    }
    public static void method2(){
        synchronized (object){
            // 同步代码块2
        }
    }
}
  1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录,内部可以存储锁定对象的Mark Word


    并发编程(五):深入理解synchronized_第5张图片
    image.png
  2. 让锁记录中Object Reference指向锁对象,并尝试使用cas替换Object的Mark Word的值存入锁记录


    并发编程(五):深入理解synchronized_第6张图片
    image.png
  3. 如果替换成功,对象头中存储了锁记录地址和状态00(代表轻量级锁),表示该线程给对象加锁,--(object对象头中Mark Word发生变化,原先01无锁状态变成00轻量级锁状态,另外分代年龄等信息变为锁记录地址),--(锁记录里面将记录hash码、分代年龄等信息)(--锁记录和Mark Word信息互换)


    并发编程(五):深入理解synchronized_第7张图片
    image.png
  4. 如果cas失败,分为两种情况
    4.1 如果是其他线程已经持有了该Object的轻量级锁,这时表名有竞争,进入锁膨胀过程
    4.2 如果是自己执行了synchronized表示锁重入,那么再添加一条Lock Record作为重入的计数


    并发编程(五):深入理解synchronized_第8张图片
    image.png
  5. 当退出synchronized代码块(解锁时),如果取值为null的锁记录,这时重置锁记录,表示重入计数减一


    并发编程(五):深入理解synchronized_第9张图片
    image.png
  6. 当退出synchronized代码块(解锁时),如果取值不为null的锁记录,这时cas将Mark Word的值恢复给对象头
  • 成功,则解锁成功
  • 失败,说明轻量级锁进程锁膨胀已经升级为重量级锁,进入重量级锁的解锁流程
    重量级锁加锁、解锁流程
  • 如果在尝试加轻量级锁的过程中,cas操作无法成功,这时一种情况是其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁升级为重量级锁
public class WeightLock {
    static final Object object = new Object();

    public static void method1(){
        synchronized (object){
            // 同步代码块1
        }
    }
}
  1. 当Thread-1进入轻量级锁加锁时,Thread-0已经对该对象加了轻量级锁


    并发编程(五):深入理解synchronized_第10张图片
    image.png
  2. 这时Thread-1加锁失败,进入锁膨胀流程
    2.1 为Object对象申请Monitor锁,让Object指向重量级锁地址
    2.2 然后线程Thread-1进入Monitor的EntryList阻塞(BLOCKED)状态


    并发编程(五):深入理解synchronized_第11张图片
    image.png
  3. 当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程
    重量级锁竞争时自旋优化
  • 重量级锁竞争时候,可以通过自旋来进行优化,如果当前线程自旋成功(即这时保持锁的新城已经退出了同步代码块。释放了锁),这时当前线程就可以避免阻塞
  • 以牺牲CPU,多核CPU情况才能发挥优势
  • Java6之后自旋是自适应的,比如对象刚刚一次自旋成功过,就认为自旋成功可能性大,就多自旋几次;反之少自旋甚至不自旋。
  • Java7之后不能控制是否开启自旋功能
  • 自旋失败后,进入EntryList阻塞(BLOCKED)状态
    并发编程(五):深入理解synchronized_第12张图片
    自旋重试成功.png

    偏向锁优化
  • 轻量级锁不需要Monitor对象,使用线程栈帧中的锁记录充当轻量级锁
  • 轻量级锁在发生锁重入的时候,是需要cas操作,插入一条值为null的锁记录


    并发编程(五):深入理解synchronized_第13张图片
    image.png
  • Java6引入偏向锁:只有第一次使用cas将线程id设置为对象头的Mark Word头,之后发现这个线程di是自己的就表示没有竞争,不用重新cas操作。以后只要不发生竞争,这个对象就归该线程所有


    并发编程(五):深入理解synchronized_第14张图片
    image.png
  • 当创建一个对象时:
    • 如果开启了偏向锁(默认开启),那么对象创建后,markword值最后三位是101,这是它的thread、epoch、age都为0
    • 偏向锁默认是延迟的,不会再程序启动时立即生效,如果想避免延迟,可以加-XX:BiasedLockingStartupDelay=0来禁用延迟
    • 如果没有开启偏向锁,那么对象创建后,markword值最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值
      验证偏向锁
/**
 * @author magw
 * @version 1.0
 * @date 2020/4/12 下午3:40
 * @description: No Description
 *  偏向锁+对象内存大小
    1).大端存储:大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。
    2).小端存储:小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
 */
@Slf4j(topic = "ants.TestBiased")
public class TestBiased {
    public static void main(String[] args) {
        Object i = new Object();
        System.out.println(Integer.toHexString(i.hashCode()));
        System.out.println(ClassLayout.parseInstance(i).toPrintable());
    }
}
2280cdac
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 ac cd 80 (00000001 10101100 11001101 10000000) (-2134004735)
      4     4        (object header)                           22 00 00 00 (00100010 00000000 00000000 00000000) (34)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  1. 从控制台看,hacode(2280cdac)和对象头(accd8022)hashcode(对象头25-56)内容不符合,这是因为我电脑是小端存储,即数据低地址存放数据地位,应该反来读取,即(2280cdac),这时对象头和hashcode对应起来


    image.png
  2. 但是我们发现对象头最低3位是001,代表是正常状态,并不是偏向锁状态?这时因为偏向锁默认是延迟的,可以采取两种方式进行来显示出对象头中的偏向锁状态
    2.1 程序启动时,在对象new之前睡眠>=4秒,例如Thread.sleep(5000);
    2.2 设置vm启动参数,-XX:BiasedLockingStartupDelay=0
    2.3 关闭偏向锁:-XX:-UseBiasedLocking
  3. 线程在修改完对象头中信息后,如果再次访问,对象头中的线程id不会变
public class TestBiased {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object i = new Object();
        System.out.println(ClassLayout.parseInstance(i).toPrintable());
    }
}

并发编程(五):深入理解synchronized_第15张图片
image.png

撤销偏向锁

  1. 如果调用锁对象的hashcode()方法,会撤销对象偏向锁,如果遇到同步,直接升级为轻量级锁。调用完hashcode后,线程对象头没有地址存放hashcode值。轻量级锁调用hashcode后会将hashcode保存到锁记录里,重量级锁调用hashcode,会将hashcode保存到moitor里。
  2. 其他对象使用拥有偏向锁对象(两个线程交叉执行),偏向锁会升级轻量级锁,当代码执行后,对象的偏向锁会撤销
  3. 调用wait/notify后,也会撤销偏向锁
@Slf4j(topic = "ants.CannelBiasedMethod")
public class CannelBiasedMethod {
    public static void main(String[] args) {
        Object o = new Object();
        new Thread("t1") {
            @Override
            public void run() {
                log.debug(ClassLayout.parseInstance(o).toPrintable());
                synchronized (o){
                    log.debug(ClassLayout.parseInstance(o).toPrintable());
                }
                log.debug(ClassLayout.parseInstance(o).toPrintable());
                synchronized (CannelBiasedMethod.class){
                    CannelBiasedMethod.class.notify();
                }
            }
        }.start();
        new Thread("t2") {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (CannelBiasedMethod.class){
                    CannelBiasedMethod.class.wait();
                }

                log.debug(ClassLayout.parseInstance(o).toPrintable());
                synchronized (o){
                    log.debug(ClassLayout.parseInstance(o).toPrintable());
                }
                log.debug(ClassLayout.parseInstance(o).toPrintable());
            }
        }.start();
    }
}

批量重偏向

  • 如果对象虽然被多个线程访问,但是没有竞争,这是偏向了线程t1的对象仍有机会重新偏向t2,重偏向会重置对象的Thread ID
  • 当撤销偏向锁阈值超过20次后,jvm会在给这些对象加锁时重新偏向至加锁线程
  • 线程1给对象线程id修改,t2线程获得对象的线程id不变,t2在执行同步代码块时,锁升级为轻量级锁,同步代码块执行完,对象变成无锁状态
  • 在达到20次后,后面的对象中的线程id就会被修改,不会再偏向锁-轻量级锁-无锁状态转化
//查看t2线程执行的执行的第19和20次,发现线程id变化,在t2后面的线程id,不在会变化
@Slf4j(topic = "ants.BatchBiased")
public class BatchBiased {
    static Thread t1,t2;
    public static void main(String[] args) {
        int count = 39;
        Vector vector = new Vector<>();
        t1 = new Thread("t1"){
            @Override
            public void run() {
                for (int i = 0; i < count; i++) {
                    Object o = new Object();
                    vector.add(o);
                    synchronized (o) {
                        log.debug(i+"\t-----------"+ClassLayout.parseInstance(o).toPrintable());
                    }
                }
                LockSupport.unpark(t2);
            }
        };
        t1.start();

        t2 = new Thread("t2"){
            @Override
            public void run() {
                LockSupport.park();
                for (int i = 0; i < count; i++) {
                    Object o =vector.get(i);
                    log.debug(i+"\t"+"before "+ClassLayout.parseInstance(o).toPrintable());
                    synchronized (o) {
                        log.debug(i+"\t"+ClassLayout.parseInstance(o).toPrintable());
                    }
                    log.debug(i+"\t"+"after "+ClassLayout.parseInstance(o).toPrintable());
                }
            }
        };
        t2.start();
    }
}
 
 

批量撤销

  • 当撤销偏向锁超过40次后,类再新建的都是不可偏向的对象。
    锁消除
  • JVM会在JIT字节码进一步优化,会在字节码层面优化。会优化掉部分代码。
  • xx:-EliminateLocks,如果能确认某个加锁的对象不会逃逸出局部作用域,就可以进行锁删除。.

你可能感兴趣的:(并发编程(五):深入理解synchronized)