Synchronized 精讲 (4)

利用 javap -verbose 类的名字查看编译后的文件

图片

方法的同步并没有通过指令monitorentermonitorexit来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象,其实底层还是monitor对象锁。

5.Java虚拟机对synchronized的优化

从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。所以synchronized关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。

5.1 锁主要存在的四种状态

无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

锁的膨胀过程

无锁状态 -> 偏向锁 -> 轻量级锁 -> 重量级锁

只能从低到高升级,不会出现锁的降级

5.2 自旋锁

所谓自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。(减少线程切换)

使用场景: 自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。

缺点:虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,所以增加了适应性自选锁

5.3 适应性自旋锁

所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

线程如果自旋成功了,那么下次自旋的次数会更加多,因为上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,很少能够成功,那么以后自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

5.4 锁消除

为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。作为写程序的人应该会知道哪里存在数据竞争,不可能随便的加锁。

5.5 锁粗化

将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。虽然我们平时倡导把加锁的片段尽量小为了增加并发效率和性能。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化。

5.6 偏向锁

在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁。

引入偏向锁主要目的是:为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:

暂停拥有偏向锁的线程

判断锁对象是否还处于被锁定状态,否,则恢复到无锁状态(01),以允许其余线程竞争。是,则挂起持有锁的当前线程,并将指向当前线程的锁记录地址的指针放入对象头,升级为轻量级锁状态(00),然后恢复持有锁的当前线程,进入轻量级锁的竞争模式

偏向锁的获取和撤销流程:

图片

5.7 轻量级锁

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpjdfy.html