synchronized可修饰的对象

  • 类的实例方法:修饰类的实例方法时,锁的是方法对应是实例对-象,粒度的对象
  • 类的静态方法:锁的是方法区里类的全局Class对象
  • 静态代码块:锁的是括号里的对象

简单的说,synchronized关键字可以修饰方法和一段代码。修饰方法是通过虚拟机向方法表中添加ACC_SYNCHRONIZED访问标志实现的,一段代码的同步是通过monitorenter和monitorexit指令实现的。

具体实现

Java虚拟机可以支持方法级的同步和方法内部一段指令的同步。两种同步都是通过使用管程(Monitor)来实现的。
方法级的同步是隐式的,无需通过字节码指令来控制。虚拟机通过向方法常量池的方法表结构中添加ACC_SYNCHRONIZED访问标志来标识这个方法为同步方法。方法调用时也会先检查这个标志,如果设置了,那么调用线程需要先成功持有管程,执行完以后需要释放管程。

Java虚拟机通过两个命令monitorenter和monitorexit来支持一段指令序列的同步。当代码中编写了synchronized关键字时,虚拟机会在代码块开始前添加一条monitorenter指令,结束时添加monitorexit指令。

Monitor管程

何为Monitor?在对象头的Mark Word中存在一个32bit的重量级锁指针,指向的是该对象的Monitor起始地址。也就是说,虚拟机为每一个对象都创建了一个monitor,当这个对象的monitor被某个线程持有时,他就处于锁定状态。
等待获取monitor的线程会被封装成ObjectWaiter进入_entrylist队列,而持有monitor的线程如果调用wait方法释放锁,那么会进入_WaitSet列表中等待被唤醒。

重量级锁的优化

锁自旋

线程在等待获取锁时会阻塞,阻塞是比较消耗系统性能的操作。因为挂起线程和恢复线程都要从用户态切换到内核态,切换的过程时间长,成本比较高。虚拟机如果预设当前等待的线程不久之后就会获得锁,为了不让等待的线程进入内核态,就会让线程进行一些空循环,来保证可以分配到cpu时间,防止其进入内核态。但是这么做需要机器是多核或者一个cpu但支持多核。自旋不是无限的,有一定的次数限制。jdk1.6之后还引入了自适应自旋锁,根据当前程序的执行情况,来决定是否要自旋,自旋多久。

锁消除

锁消除是基于对变量的动态作用域的分析,虚拟机判定不可能出现竞态条件,会去除代码中已经写了的锁。锁消除主要是基于虚拟机的逃逸分析技术实现。
逃逸分析技术是虚拟机对变量的作用域进行动态分析,如果变量会作为方法返回值或者参数,那么就是方法逃逸,还有实例逃逸,线程逃逸等等情况。如果可以确定局部变量不会逃逸,虚拟机可以针对其做很多优化,例如:

  • 栈上分配:将局部使用的对象的内存直接分配在栈上而不是堆上,让对象随栈销毁而销毁,减少垃圾回收系统的压力
  • 同步消除:消除一个线程局部变量的同步限制,即消除锁
  • 标量替换:如果对象的其他属性没有被用到,那么在局部操作时不初始化这个对象,而是对对象的标量属性进行初始化。标量即原始数据类型。
    目前逃逸分析算法还有很多可优化的地方,算法的准确度,分析所带来的消耗能不能小于系统原先的消耗,等等。因此很多虚拟机默认不开启逃逸分析。锁消除应该也运用的并不广泛。

锁粗化

将一系列的加锁操作,粗化成,将锁加在系列操作的开始之前,解锁在结束之后。例如一系列的stringbuffer的append操作,append方法本身是带sychronized的,如果连续append,那么虚拟机会自动将sychronized加在第一个append操作之前,并且在最后一次append之后解锁。

偏向锁

研究发现大部分的锁并没有产生激烈的竞争,而是由同一个线程反复获取。因此虚拟机在线程获取一个对象的锁之后,会将线程id记录在对象的头部Mark Word里面,线程重复获取锁时不需要产生进行同步的消耗。

搬运自我的简书 https://www.jianshu.com/p/7ce202c56b20