线程安全(一)java对象头分析以及锁状态

系列文章目录

线程安全(一)java对象头分析以及锁状态
线程安全(二)java中的CAS机制
线程安全(三)实现方法sychronized与ReentrantLock(阻塞同步)
线程安全(四)Java内存模型与volatile关键字
线程安全(五)线程状态和线程创建
线程安全(六)线程池
线程安全(七)ThreadLocal和java的四种引用
线程安全(八)Semaphore
线程安全(九)CyclicBarrier
线程安全(十)AQS(AbstractQueuedSynchronizer)

0.准备

  1. jdk1.8环境
  2. 引入OpenJDK的jar包
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>

1.新建对象在内存中的情况

代码示例:

    public static void main(String[] args) {
        Object o = new Object();
        System.out.println("new Object:" + ClassLayout.parseInstance(o).toPrintable());
        A a = new A();
        System.out.println("new A:" + ClassLayout.parseInstance(a).toPrintable());
        a.setFlag(true);
        a.setI(1);
        a.setStr("ABC");
        System.out.println("赋值 A:" + ClassLayout.parseInstance(a).toPrintable());
    }
    static class A{
        private boolean flag;
        private int i;
        private String str;

        public boolean isFlag() {
            return flag;
        }
        public void setFlag(boolean flag) {
            this.flag = flag;
        }
        public String getStr() {
            return str;
        }
        public void setStr(String str) {
            this.str = str;
        }
        public int getI() {
            return i;
        }
        public void setI(int i) {
            this.i = i;
        }
    }

运行结果:

在这里插入图片描述
概念
对象在内存中分为三个部分

  • 对象头(Header)
  • 实例数据(Instace Data)(可为空)
  • 对齐填充(Padding)(非必须)

分析

  • 新建Objec对象时,在内存里占用16个字节,其中Header占12个(markword占8个+classpointer占4个),没有实例数据,补充对齐4个。
  • 新建对象A时,中Header占12个(markword占8个+classpointer占4个),实例数据中 boolean占一个字节,会补齐三个,int占4个,String占8个,无需补充对齐。

结论

  • 新建Object对象,会在内存占用16个字节,其中Header占12个(markword占8个+classpointer占4个),没有实例数据,补充对齐4个。
  • 如果对象头+实例数据的字节数能被8整除,则不需要补充对齐。

附:
-XX:+UseCompressedClassPointers(64位虚拟机ClassPointer是8个字节(64位),默认此压缩类指针指令是开启的,占4个字节(32位))
-XX:+UseCompressedOops (默认压缩类普通对象指针是开启的,占4个字节(32位))

2.MarkWord与锁升级

第一个字节中8bit

附:《深入理解java虚拟机第二版》图

在这里插入图片描述
在这里插入图片描述

2.1.偏向锁和轻量级锁

代码示例

 public static void main(String[] args) {
        Object lightObject = new Object();
        try {
            Thread.sleep(6000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object biasedLockObject = new Object();
        System.out.println("---------------------------------------加锁前---------------------------------------");
        System.out.println("偏向锁:" + ClassLayout.parseInstance(biasedLockObject).toPrintable() + "\n轻量级锁:" + ClassLayout.parseInstance(lightObject).toPrintable());
        System.out.println("---------------------------------------加锁后---------------------------------------");
        synchronized (biasedLockObject) {
            System.out.println("偏向锁:" + ClassLayout.parseInstance(biasedLockObject).toPrintable());
        }
        synchronized (lightObject) {
            System.out.println("轻量级锁:" + ClassLayout.parseInstance(lightObject).toPrintable());
        }
        System.out.println("---------------------------------------释放锁---------------------------------------");
        System.out.println("偏向锁:" + ClassLayout.parseInstance(biasedLockObject).toPrintable() + "\n轻量级锁:" + ClassLayout.parseInstance(lightObject).toPrintable());
    }

运行结果

偏向锁和轻量级锁

拿出对象Header第一个字节进行分析

在这里插入图片描述

概念

  1. 默认情况,偏向锁有时延,直接建立对象为无锁-》轻量级锁-》无锁
  2. 增加延迟,偏向锁-》偏向锁-》偏向锁,同一个的对象每次运行时,加锁前对象头一样,加锁后和释放后是不一样的,因为线程把对象头里线程Id改为了自己线程的Id
  3. 轻量级锁执行过程:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前的MarkWord的拷贝(Displaced Mark Word),然后,虚拟机将使用CAS操作阐释将对象的Mark Word更新为指向Lock Record的指针,如果成功,该线程就拥有了该对象的锁,失败,则首先会检查对象的Mark Word是否已经指向当线程的栈帧,有就代表已拥有,直接进入同步,否则,被其他线程占用
  4. 轻量级锁解锁过程:通过CAS过程将对相当前的MarkWord与线程中的Displaced MarkWord替换回来,如果替换成功,则同步成功,失败,则说明有其他线程尝试获取过锁,就要在释放锁的同时,唤醒等待线程

分析

  1. JVM默认,偏向锁有时延,sleep一段时间后,就会开启偏向锁,也可以设定参数-XX:BiasedLockingStartupDelay=0
  2. 上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程 偏向锁不可重偏向 批量偏向 批量撤销
  3. 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,偏向级锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。这个锁会偏向于第一个获得锁的线程,如果在接下来执行的过程中,该锁都没有被其它线程获取,则持有偏向锁的线程将永远不需要在进行同步。
  4. 当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式,同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步模块时,虚拟机都可以不再进行任何同步操作(例如 Locking UnLocking以及对Mark Word的Update等)。当有另一个线程尝试获取这个锁的时候,偏向模式就宣告结束。
    根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标记为“01”)或轻量级锁定(标记为“00”)的状态,后续的同步操作就如轻量级锁那样执行。

2.2.偏向锁膨胀为重量级锁

代码示例

static Object object = null;

    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().setName("线程0");
        Thread.sleep(6000);
        object = new Object();
        System.out.print("初始偏向锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
        sysn("线程1");
        Thread.sleep(1000);
        System.out.print("线程1占用,未有其他线程尝试获取锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
        sysn("线程2");
        Thread.sleep(1000);
        System.out.print("线程1占用,线程2尝试获取锁后->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
        Thread.sleep(10000);
        System.out.println("释放锁->" + "线程名称:" + Thread.currentThread().getName() + ClassLayout.parseInstance(object).toPrintable());
    }

    private static void sysn(String threadName) {
        new Thread(() -> {
            Thread.currentThread().setName(threadName);
            synchronized (object) {
                System.out.print(threadName + "占用中->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

运行结果
在这里插入图片描述

分析

  1. 初始化对象可偏向状态,线程1占用时,变为偏向锁,其他线程也尝试占用时,膨胀为重量级锁,没有线程占用之后,对象状态还是膨胀状态

2.3.轻量级锁锁膨胀为重量级锁

代码示例

static Object object = null;

    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().setName("线程0");
        object = new Object();
        System.out.print("无锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
        sysn("线程1");
        Thread.sleep(1000);
        System.out.print("线程1占用,未有其他线程尝试获取锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
        sysn("线程2");
        Thread.sleep(1000);
        System.out.print("线程1占用,线程2尝试获取锁后->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
        Thread.sleep(10000);
        System.out.println("释放锁->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
    }

    private static void sysn(String threadName) {
        new Thread(() -> {
            Thread.currentThread().setName(threadName);
            synchronized (object) {
                System.out.print(threadName + "占用中->" + "线程名称:" + Thread.currentThread().getName() + "\n" + ClassLayout.parseInstance(object).toPrintable());
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

运行结果

在这里插入图片描述

分析

  1. 初始化对象为无锁状态,线程1占用时,变为轻量级锁,其他线程也尝试占用时,膨胀为重量级锁,没有线程占用之后,对象状态还是膨胀状态

下图为《深入理解java虚拟机第二版》的图
在这里插入图片描述借用一张网图,侵删

在这里插入图片描述

3.锁优化

3.1自旋锁与自适应自旋

自旋锁避免线程切换的开销,党要占用处理器的时间,锁被占用时间短,自旋等待好,占用时间长,白白消耗处理器资源,自选默认10次

3.2锁消除

代码上要求同步,但是被检测到不可能存在共享数据竞争 String.concatString()

3.3锁粗化

如果虚拟机检测到一系列操作都是对同一个对象反复加锁和解锁,将会把加锁的范围扩展到这个操作序列外部例如String的多个append()方法

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值