Java并发编程的艺术学习笔记(三)

Java并发机制的底层实现原理(synchronize)

1.synchronize

1.1 synchronize的作用

●保证线程之间能够实现互斥访问

●保证共享变量的可见性问题

●保证处理器的重排序不影响并发编程。

1.2 synchronize实现同步的基础

Java中的每一个对象都可以作为锁,主要有以下三种表现形式:

●对于普通方法来说,锁是当前实例对象。

●对于静态方法来说,锁是当前类的Class对象。

●对于同步块来说,锁是synchronize括号里的对象。

2. Java对象头

Java对象头主要由两部分组成:Mark Work 和类型指针。

● Mark Work :默认存储对象的HashCode、分代年龄、锁标记位。随着锁标记位的改变,Mark Work存储的数据也会发生改变。

●类型指针:类型指针指向对象的类型数据,即指明该对象是由哪个类实例出来的。

3. 锁的升级与对比

Java SE 1.6为了减少锁的获取与释放带来的开销,引入了“偏向锁”和“轻量级锁”。此时锁一共有四种状态(按级别从低到高排序):无锁状态、偏向锁、轻量级锁、重量级锁。这几个状态会随着锁的竞争而升级,并且锁可以升级但不能降级。此外需要指出的是:synchronize用的锁是存在Java对象头里的。

3.1 偏向锁

大量研究表明,大多数情况下并不存在多线程竞争同一把锁,锁通常自始至终由同一个线程获得。在这种情况下,锁的获取和释放将是比较大的开销。为了优化锁的开销,偏向锁被引入。当一个线程第一次获取锁时,锁对象就进入了偏向模式,同时使用CAS操作把当前线程ID存储在锁对象的 Mark Dowm 中。若操作成功,持有偏向锁的线程每次进入和这个偏向锁相关的同步块时,只需要简单测试一下对象头的 Mark Down 里是否存储着该线程ID。如果测试成功,表示线程获得了锁。当有另外的线程尝试获取这个锁时,偏向锁失效。并且在全局安全点拥有该锁的线程将被暂停,同时检查该线程是否还在活动状态。若不存活,则将对象锁设置为无锁状态;若存活,则继续执行原先获得偏向锁的线程,对象头的 Mark Down 要么重新偏向其他线程,要么被标志位无锁或是不适合作为偏向锁。

3.2 轻量级锁

当一个线程准备获取对象锁并且对象处于无锁状态时,JVM在线程的栈帧中建立名为“锁记录”的空间。同时把对象头里的 Mark Down 复制到锁记录里,官方称之为Displaced MarK Down,然后线程使用CAS操作尝试把 Mark Down 更新为指向线程所记录的指针。若操作成功,表面此线程获得锁;若失败,说明锁被其他线程抢占,此时轻量级锁失效且将膨胀成重量级锁。

4. 锁的优化

引入锁优化的背景:Java的线程是被映射到操作系统原生的线程上的,所以线程的阻塞和唤醒需要操作系统的介入,这里面就涉及到了用户态和内核态的相互转换,又因为这种状态转换开销很大,故引起线程阻塞的重量级锁开销很大。JVM针对这种情况也会进行一些优化,譬如在操作系统阻塞线程之前先让线程进行自旋,避免频繁的切入带内核态。

4.1 自旋锁

线程的阻塞对系统性能的影响是很大的,并且很多时候共享数据的加锁状态只需要持续很短的时间,若为了这段时间去阻塞和唤醒线程是不值当的,所以自旋锁的作用就是把因同步操作而需要阻塞的线程先自旋一段时间(即忙等待),若在自旋的时间段内还不能获得锁,再进行线程的阻塞。

4.2 锁消除

锁消除是指虚拟机即时编译器对于一些代码上要求同步而实际上不存在数据竞争的锁进行消除。

4.3 锁的粗化

一般来说,推荐在编写同步代码块的时候尽量把同步块的作用范围缩小,目的是使需要同步的代码量尽量小,若是存在锁竞争,也便于其他线程尽快获得锁。以上原则在大部分情况下是正确的,但存在这么一种情况:对一个对象进行一系列反复的加锁和解锁操作,且这些操作都是发生在循环体或是相近的代码块里,这样的同步操作会增加很多多余的性能损耗。锁粗话即把同步的范围扩大(粗化)在这一系列需要同步的代码块以外,这样就只需一次加锁解锁的同步操作即可。

5. 锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗 若线程间存在锁竞争,则会有额外的撤销锁的消耗 同一个线程多次访问同步代码块
轻量级锁 竞争锁的线程不必阻塞 线程无法成功竞争锁的情况下使用自旋会消耗CPU 多个线程交替获取执行同步块
重量级锁 不使用自旋不消耗CPU 线程阻塞和唤醒的开销大且相应时间慢 追求吞吐量、同步块执行时间长

猜你喜欢

转载自juejin.im/post/5cde0b64e51d453a572aa305