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

0.准备

1.jdk1.8环境
2.引入OpenJDK的jar包

        
            org.openjdk.jol
            jol-core
            0.9
        

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;
        }
    }

结果
线程安全(一)java对象头分析以及锁状态_第1张图片

 概念
 对象在内存中分为三个部分
 1. 对象头(Header)
 2. 实例数据(Instace Data)(可为空)
 3. 对齐填充(Padding)(非必须)
分析
 1. 新建Objec对象时,在内存里占用16个字节,其中Header占12个(markword占8个+classpointer占4个),没有实例数据,补充对齐4个。
 2. 新建对象A时,中Header占12个(markword占8个+classpointer占4个),实例数据中 boolean占一个字节,会补齐三个,int占4个,String占8个,无需补充对齐。
 结论
 1.新建Object对象,会在内存占用16个字节,其中Header占12个(markword占8个+classpointer占4个),没有实例数据,补充对齐4个。
 2.如果对象头+实例数据的字节数能被8整除,则不需要补充对齐。
 附:
 -XX:+UseCompressedClassPointers(64位虚拟机ClassPointer是8个字节(64位),默认此压缩类指针指令是开启的,占4个字节(32位))
 -XX:+UseCompressedOops (默认压缩类普通对象指针是开启的,占4个字节(32位))

2.MarkWord与锁升级

第一个字节中8bit

线程安全(一)java对象头分析以及锁状态_第2张图片附:《深入理解java虚拟机第二版》图
线程安全(一)java对象头分析以及锁状态_第3张图片

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());
    }

结果
线程安全(一)java对象头分析以及锁状态_第4张图片
只拿出对象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();
    }

线程安全(一)java对象头分析以及锁状态_第5张图片

分析
 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();
    }

线程安全(一)java对象头分析以及锁状态_第6张图片

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

下图为《深入理解java虚拟机第二版》的图
线程安全(一)java对象头分析以及锁状态_第7张图片借用一张图

3.锁优化

3.1自旋锁与自适应自旋

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

3.2锁消除

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

3.3锁粗化

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

你可能感兴趣的:(JAVA多线程与高并发)