Java并发编程:synchronized机制
1. 简介
synchronized
是 Java 中最基本的一种同步机制,是 Monitor 机制(监视器机制或管程机制)在 JVM 中的实现,使用 synchronized
时关联的 Java 对象被称为监视器对象, 而被 synchronized
保护的代码被称为同步代码块(或者临界代码区),同一时刻只能有一个线程执行同步代码块,synchronized
有四个特性:
- 原子性:被
synchronized
保护的代码块的执行式原子的; - 可见性:在同步代码块中被修改的共享变量会在锁释放之前刷新到主存当中,保证资源变量的可见性;
- 有序性:Java允许编译器和处理器对指令进行重排序,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性,但是被
synchronized
保护的同步代码块,在执行过程中实际上被串行化,同一时刻只允许有一个线程执行同步代码块,因此synchronized
可以保证同步代码块的有序性; - 可重入性:
synchronized
使用的锁是可重入的,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
2. Monitor 机制简介
Monitor 机制主要包含两种机制
- 互斥同步机制:由 Monitor 机制同步的临界区,线程之间只能进行互斥访问,每一时刻只有一个线程可以进入临界区;
- 等待唤醒机制:又叫条件机制,由于 Monitor 机制采取互斥同步机制,因此,为了防止线程等待其他资源而长时间占有锁,Monitor 机制还包含条件机制,线程可以在某条件未发生时,先挂起阻塞并释放锁给其他线程,等待条件发生时再被唤醒;
实现互斥同步机制需要互斥锁,实现条件机制需要条件对象来提供提供等待通知功能。
3. 监视器对象
前文介绍了 synchronized
是 JVM 中的 Monitor 机制,而管程机制需要互斥锁和条件对象,在 JVM 中互斥锁和条件对象的角色都由 Java 对象来扮演,作为互斥锁和条件对象的 JAVA 对象被称为监视器对象,任何 Java 对象都可以作为监视器对象,监视器对象同时承担互斥锁和条件对象的角色,我们看看具体是如何实现的。
Java 对象头
在 JVM 中,Java 对象包含三个部分:对象头、实例数据、对齐填充,其中对象头一般由 8 个字节 64 个比特位组成,其中前 4 个字节 32 个比特位是标记域(Mark Word),Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位,其中最后两个比特位就是锁标记位,Mark Word 里面的内容会随着锁标记位的变化而变化。
监视器对象的锁策略
在 synchronized
管程机制中,监视器对象扮演了锁的角色,在 JDK1.6 之前,监视器的互斥锁只采用了重量级锁策略,而在 JDK1.6 之后,JVM对监视器锁进行了优化,采用混合锁策略并且实现了锁膨胀机制,我们先介绍各种锁策略。
- 重量级锁:使用底层操作提供的互斥量来实现锁,比如 linux 操作系统的 mutex,由于重量级锁需要使用底层操作系统提供的资源,因此需要进行系统调用,从而产生用户态到内核态的切换,而且竞争锁失败的线程会被操作系统挂起,等待锁释放;当对象头中的锁标记位为
0b10
时,对象头中 Mark Word 其他比特位的内容变成指向系统互斥量的指针。 - 轻量级锁:使用进程内的私有变量 + CAS操作来实现锁,比如锁标记位为
0b00
时,表示使用轻量级锁,轻量级锁通过基于 CAS 操作的同步算法来修改对象头的 Mark Word 域,修改成功,说明当前线程获取锁,如果修改失败,说明发生了竞争,于重量级锁不同,轻量级锁竞争失败后,需要调用方对竞争失败的线程进行同步处理,JVM 采取了自旋重试的策略。 - 偏向锁:偏向锁假定将来只有第一个申请锁的线程会使用锁,因此将第一个获取锁的的线程ID通过 CAS 记录在 Mark Word 中,如果记录成功,则偏向锁获取成功,将锁标记位改为
0b01
,之后如果申请锁的线程ID等于偏向锁绑定的线程ID,就可以直接进入临界区。
锁膨胀机制
目前的JVM对监视器锁采取锁膨胀或锁升级机制,根据实际情况对锁策略进行膨胀升级,其升级方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,锁策略升级之后是无法降级的。具体升级的过程如下:
1.