并发编程synchronized学习

本篇只是基于我这几天的并发编程学习笔记,只是个人理解,可能有误,看到请指正

1.java头信息

1.JOL 使用

引入jar包

		<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>
使用jol查看对象的内存布局:
ClassLayout.parseInstance(testObjectSize).toPrintable()

2.java头信息

Java对象保存在内存中时,由以下三部分组成:
1,对象头
2,实例数据
3,对齐填充字节
java的对象头由以下三部分组成:
1,Mark Word
2,klass word 指向类class对象的指针
3,数组长度(只有数组对象才有)
4,对齐填充(8字节对齐,差了就补)
64位系统下的头信息:
在这里插入图片描述

1.无锁:未用25+hashcode31+未用1+4(对象年龄)+偏向锁标识1+锁状态2
2.偏向锁:偏向内核线程id 54+epoch(偏向记录)2+未用1+4(对象年龄)+偏向锁标识1+锁状态2
3.轻量级锁:栈中锁记录的指针62+锁状态2
4.重量级锁:线程Monitor的指针62+锁状态2
最后三位 101 偏向锁,001 无锁,00轻量锁,10重量锁,11 GC标识
java使用的是小端存储,即高位低存

21685669
cn.mlg.test.jol.Test2 object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 a5 e5 4a (00000001 10100101 11100101 01001010) (1256563969)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           28 30 41 21 (00101000 00110000 01000001 00100001) (557920296)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)

hashcode :21685669(十进制)
Mark Word信息,前8个字节00000000 00000000 00000000 00000001 01001010 11100101 10100101 00000001

对象大小计算:
Mark Word 8字节
klass word 开启指针压缩为4字节 没开启8字节
数组长度 4字节没开启8字节
对齐填充
非静态实例数据
引用类型 4字节没开启8字节
基本数据根据其长度计算
-XX:+UseCompressedOops 开启指针压缩(默认)
-XX:-UseCompressedOops (关闭指针压缩)
指针压缩堆内存范围是4G-32G,

2.synchronized关键字

java -v 查看指令
synchronized在编译时会转换成monitorenleter指令,关于monitorenleter的jvm规范:objectref必须是引用类型。每个对象都有一个与其关联的监视器。执行monitorenter的线程获得与objectref关联的监视器的所有权。如果另一个线程已经拥有与objectref关联的监视器,则当前线程将等待直到对象被解锁,然后再次尝试获取所有权。如果当前线程已经拥有与objectref关联的监视器,则它将在监视器中增加一个计数器,以指示该线程进入监视器的次数。如果与objectref关联的监视器不属于任何线程,则当前线程将成为监视器的所有者,并将此监视器的条目计数设置为1
https://docs.oracle.com/javase/specs/jvms/se6/html/Instructions2.doc9.html#monitorenter

1.锁概念

jdk在1.6之前synchronized一直是重量级锁、悲观锁,互斥锁,在1.6之后优化了一下分成偏向锁,轻量级锁、重量级锁
重量级锁:内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex),它每次会调用系统函数(待验证)引起的内核态与用户态切换、线程阻塞造成的线程切换、使用成本比较高。在多个线程竞争的时候synchronized关键字会膨胀为重量级锁

public static void main(String[] args) throws InterruptedException {
       
        DemoA demoA = new DemoA();
        Thread thread1 = new Thread(){
            public void run() {
                synchronized (demoA){
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                }
            }
        };
        thread1.start();
        
        Thread thread2 = new Thread(){
            public void run() {
                synchronized (demoA){
                    System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                }
            }
        };
        thread2.start();
       
    }
    

轻量级锁:如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,比如交替运行线程,只会调用一次os函数


        DemoA demoA = new DemoA();
        Thread thread1 = new Thread(){
            public void run() {
                synchronized (demoA){

                    System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                }
            }
        };
        thread1.start();
        thread1.join();
        new Thread().start();
        Thread thread2 = new Thread(){
            public void run() {
                synchronized (demoA){
                    System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                }
            }
        };
        thread2.start();

    }

中间new Thread().start();是为了不让线程id相同,这个线程id不是java线程id,如果不加,运行可能导致线程id线程相同,都是偏向锁

偏向锁:如果不仅仅没有实际竞争,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS,不用调用os函数
有的文档将自旋也是定义一个锁,其实它不是一个状态,只是获取一个锁的过程
偏向锁和轻量级锁 都是乐观锁

public static void main(String[] args) throws InterruptedException {
//
        Thread.sleep(6000l);
        DemoA demoA = new DemoA();
        Thread thread1 = new Thread(){
            public void run() {
                synchronized (demoA){
                    System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                }
            }
        };
        thread1.start();
    }

sleep原因:JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为4s左右,具体时间因机器而异。当然我们也可以设置JVM参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁
使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)
需要注意的是如果对象已经计算了hashcode就不能偏向了,如果调用wait方法则立刻变成重量锁

2.锁的膨胀过程

 public static void main(String[] args) throws InterruptedException {
        Thread.sleep(6000l);
        DemoA demoA = new DemoA();
        System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
        Thread thread1 = new Thread(){
            public void run() {
                synchronized (demoA){
                    System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                }
            }
        };
        thread1.start();
     thread1.join();
        System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
        Thread thread2 = new Thread(){
            public void run() {

                synchronized (demoA){
                    System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                }
            }
        };
        thread2.start();
     //   thread2.join();
       // thread1.join();
        System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
      /* new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();*/

        Thread thread3 = new Thread(){
            public void run() {

                synchronized (demoA){
                    System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                }
            }
        };
        thread3.start();
        System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
        //thread3.join();
    }

1.jvm初始完创建对象,虽然是一个偏向锁状态,但是其线程id是0 也就是偏向任何线程,是一个可偏向状态

OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)

2.thread1 加了synchronized ,此时只有一个线程独享,无其他线程竞争,而且线程是可偏向状态,此时就会偏向thread1 ,将线程id修改为thread1 id,大致过程是先判断线程是否可偏向可以就偏向,不可以就判断线程id是否是相同

 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 80 2d 25 (00000101 10000000 00101101 00100101) (623738885)

偏向线程执行完成后,不会撤销,只会膨胀

 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 80 2d 25 (00000101 10000000 00101101 00100101) (623738885)

3.thread2 等thread1执行完成后同步,此时发现已经偏向了别的线程,先撤销偏向锁,改为无锁状态,在膨胀为轻量级锁,指针指向指向原线程的锁记录,用CAS将指针指向当前线程锁记录成功就获得轻量级锁,失败会自旋(循环一定次数),减少线程切换

 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           10 ee cb 25 (00010000 11101110 11001011 00100101) (634121744)

轻量锁执行完成后,会置为无锁状态,上述代码因为下面测试重量级锁,不一样

cn.mlg.test.concurrent.DemoA object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)

4.轻量级锁自旋失败就会升级为重量级锁,会将Mark Word指向monitor,monitor是一个C++对象,对C++和jvm源码不懂,只知道里面存了一个等待锁的集合和当前持有锁线程

 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a 37 2a 21 (00101010 00110111 00101010 00100001) (556414762)

重量级锁执行完成后还是重量级锁,不会撤销和降级

3.偏向锁批量撤销和批量偏向

 public static void main(String[] args) throws InterruptedException {
        Thread.sleep(6000l);
        List<DemoA> list= new ArrayList<>();

        Thread thread = new Thread(){
            @Override
            public void run() {

                for (int i = 0; i < 50; i++) {
                    DemoA demoA = new DemoA();

                    synchronized (demoA){
                        list.add(demoA);
                       /* System.out.println(Thread.currentThread().getId());
                        System.out.println(ClassLayout.parseInstance(demoA).toPrintable());*/
                       if (i==10){
                           System.out.println(ClassLayout.parseInstance(demoA).toPrintable()) ;
                       }
                    }
                }
                try {
                    Thread.sleep(50000l);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
            }
        };
        thread.start();

        try {
            //保证thread循环完成
            Thread.sleep(3000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        Thread thread1 = new Thread(){
            public void run() {

                for (int i = 0; i <40; i++) {
                    DemoA demoA = list.get(i);
                    synchronized (demoA){
                       if (i==18 ||i==19 ){
                           System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                       }
                    }
                }
                try {
                    Thread.sleep(50000l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread1.start();

        try {
            //保证thread1循环完成
            Thread.sleep(3000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread thread2 = new Thread(){
            public void run() {

                for (int i = 20; i <50; i++) {
                    DemoA demoA = list.get(i);
                    synchronized (demoA){
                        if (i==20 ||i==39 ){
                            System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                        }
                    }
                }
                try {
                    Thread.sleep(50000l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread2.start();

    }

批量重偏向好测试,
以class为单位,维护了一个偏向锁撤销计数器,每次class对象撤销就+1,当这个值达到阀值(默认20)时,jvm认为该class偏向有问题,就发送批量重偏向。主要是根据epoch的,class中维护了个epoch,对象中也有epoch(倒数第9 和第十位),对象在创建的时候取得就是class中的epoch,每次发送批量重偏向class中的epoch+1,就会遍历jvm所有栈中 在处于偏向锁加锁状态的对象(还在同步块中的),将其epoch改为class中的epoch,不在同块中的就不修改,下次获得锁时候,就去判断epoch和class中是否相等就不相等说明已经失效,就通过cas将线程id改为当前线程id

		 Thread.sleep(6000l);
        List<DemoA> list= new ArrayList<>();

        Thread thread = new Thread(){
            @Override
            public void run() {

                for (int i = 0; i < 50; i++) {
                    DemoA demoA = new DemoA();

                    synchronized (demoA){
                        list.add(demoA);
              
                       if (i==35){
                           try {
                               System.out.println("35到了");
                               Thread.sleep(50000l);
                           } catch (InterruptedException e) {
                               e.printStackTrace();
                           }
                           System.out.println("35走了");
                       
                       }

                    }
                }
                try {
                    Thread.sleep(50000l);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
            }
        };
        thread.start();

        try {
            //保证thread循环完成
            Thread.sleep(2000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        Thread thread1 = new Thread(){
            public void run() {

                for (int i = 0; i <35; i++) {
                    DemoA demoA = list.get(i);
                    synchronized (demoA){
                       if (i==20 ){
                           System.out.println(ClassLayout.parseInstance(demoA).toPrintable());
                           System.out.println(ClassLayout.parseInstance(list.get(34)).toPrintable());
                       }
                    }
                }
                try {
                    Thread.sleep(50000l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        thread1.start();
        thread.join();
        thread1.join();

批量撤销有点难重现(批量重偏向同一代码下面就是),达到了批量撤销的阈值(默认40),就会发生批量撤销,此时膨胀为轻量级级锁
-XX:+PrintFlagsInitial 可以打印启动信息
BiasedLockingBulkRebiasThreshold = 20 批量偏向的阈值
BiasedLockingBulkRevokeThreshold = 40 批量撤销的阈值
BiasedLockingStartupDelay = 4000 偏向锁延时
目前还有很多问题没清楚,学习并发需要熟悉JVM,系统和硬件有一定了解,还有很多其实没搞清楚,等我学完在更新

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值