Java并发编程-synchronized(二)

367 阅读4分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

前言

从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁膨胀、偏向锁、轻量级锁这些优化策略。

4. 锁优化

锁的状态总共四种状态, 无锁状态,偏向锁,轻量级锁重量级锁.随着锁的竞争,锁可以从偏向锁升级到轻量级锁, 在升级重量级锁,锁的升级时单向的,只能从低到高.

在 JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。

开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

4.3 自旋锁

轻量级锁失败后,虚拟机为了避免线程真实的在操作系统层面挂起,还会进行一种"自旋"的优化手段.

注意: 这个是基于在大多数情况下,线程持有锁的时间都不会太长, 如果直接挂起操作系统层面的线程可能会得不偿失.因为操作系统层面线程的切换涉及到用户态到内核态的切换, 成本较高.
因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。

所以我们在做一些业务平台组件或者一些特殊的业务场景,可以利用自选去提升程序的并发效率.

4.1 偏向锁

偏向锁是Java1.6之后加入的新锁,他是一种针对锁操作的优化手段,经研究发现,在大多数情况下,锁不仅仅存在多线程竞争,而且总是由同一个线程多次获得,因此减少同一个线程获取锁代价而引入偏向锁.

4.2 轻量级锁

若偏向锁获取失败,虚拟机不会立即升级为重量级锁,它会尝试使用一种轻量级锁的优化手段,此时Mark Word的结构也变为轻量级锁的结构.
轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。

使用场景: 轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

4.4 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间.

比如: StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

JIT
JIT是just in time的缩写,也就是即时编译。 (有关的概念可以参考:即时编译)

逃逸分析

使用逃逸分析,编译器可以对代码做如下优化:

1、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
2、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
3、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

经典面试题

是不是所有的对象和数组都会在堆内存分配空间?

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,

XX:+DoEscapeAnalysis : 表示开启逃逸分析
XX:­DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定XX:DoEscapeAnalysis