Synchronized

79 阅读3分钟

Java对象与对象头

java对象在内存中的布局

  • 对象头
  • 实例数据
  • 填充数据

对象头

  • Mark Word:记录hashCode、对象分代年龄【待办】、锁信息 (参考文章尾部的图1)
  • Klass Pointer:Class对象的指针

虚拟机在内部使用OOP-Klass模型描述一个Java类,将一个Java类一拆为二,第一个是oop(ordinary object pointer),第二个是klass。 参考链接

Synchronized

  • 加锁的几种方式

    加锁方式锁的对象实现指令
    普通方法对象本身monitorenter、monitorexit、monitorexit
    静态方法当前类的class类对象,因为class对象是全局唯一的, 会导致一方法获取到锁,其它方法等待锁的情况ACC_SYNCHRONIZED
    代码块自定义的实例对象monitorenter、monitorexit、monitorexit

Monitor

  • 每个对象被创建后,都会有一个与之对应的monitor对象
  • 当锁升级为重量锁时,就会调用monitor实现同步
  • monitor由虚拟机负责实现,实现类:ObjectMonitor.hpp 源文件链接

锁优化

  • 1.5之前只有重量级锁,每次都会通过操作系统的mutext实现同步,会涉及到 用户态、内核态切换

  • 1.6之后的锁升级: 无锁->偏向锁->轻量级锁->重量级锁 (参考文章尾部的图3、4、5)

    偏向锁

    偏向锁主要是在无多线程竞争下,解决线程重入,减少不必要的轻量级锁,并不能解决竞争问题;

    CAS设置MarkWord中的 ThreadID失败会转为轻量锁;

    偏向锁的撤销:

    偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,持有锁的线程不会主动释放偏向锁;

    需要等待全局安全点(在这个时间点上没有字节码正在执行),撤销偏向锁的时候会导致stop the world

    轻量级锁

    线程会在栈帧下创建Lock Record,拷贝Mark Word到Lock Record,用CAS+自旋的方式更新MarkWord中的锁记录指针;

    适应性自旋

    线程在获取轻量级锁CAS失败后,会尝试一定次数后,再转为重量锁;

    好处:如果持有锁的线程很快就执行完了,可以减少不必要的上下文切换

    坏处:如果线程竞争特别激烈 或 持有锁的线程长时间执行不完,这种情况线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要CPU的线程又不能获取到cpu,造成cpu的浪费。这种情况应该关闭自旋。

    JDK1.6中 -XX:+UseSpinning 开启;-XX:PreBlockSpin=10 为自旋次数; JDK1.7后,去掉此参数,由jvm控制;

    重量级锁

    Mark Word指向ObjectMonitor,利用操作系统的mutex互斥量(linux下通过pthread_mutex_lock);

    如果_owner== null || _owner == self 时,设置 _owner,退出。否则封装成ObjectWaiter,放到entryList中;

    锁消除

    加锁对象只能被一个线程访问的情况; JIT编译器的逃逸分析来消除一些不必要的锁,如使用局部变量作为锁对象(局部变量存在栈帧中)

    锁粗化

    将多个连续的锁扩展成一个范围更大的锁

  • 1.6跟1.5优化:

    偏向锁、轻量级锁,不依赖底层操作系统,没有用户态、内核态切换的消耗

  • 无锁->偏向锁->轻量级锁,实现类:synchronizer.cpp, 源文件链接

  • 轻量级锁->重量级锁,实现类:objectMonitor.cpp 源文件链接

问题

  • 全局安全点是什么?

我感觉需要运行到全局安全点是因为需要持有偏向锁线程要升级为轻量锁,升级为轻量锁需要持有偏向锁的线程在自己的栈桢中创建锁记录 LockRecord。 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。 将锁记录中的Owner指针指向锁对象。 将锁对象的对象头的MarkWord替换为指向锁记录的指针。 这些操作必须由偏向锁的持有线程在安全点上停下后才知晓要升级锁,锁升级需要由他自己完成。

segmentfault.com/a/119000001…

16771640593870_.pic.jpg

参考文章: juejin.cn/post/702803…