目录
synchronzied同步锁保证线程安全依赖于操作系统的互斥锁(Mutex Lock),消耗资源的同时还需一直等待,没有额外的尝试获取机制,不过在java1.6以后进行了优化。
1.底层实现
使用synchronized保证线程安全,其实就是通过加synchronzied锁来保证一个线程在整个执行过程中不会被其他线程干扰(原子性)。
而利用javap(可以查看java源文件)反编译后的代码中,我们可以看到,其底层实现利用了monitorenter/monitorexit来实现同步的:
11: astore_1
12: monitorenter //监视器进入
13: aload_0
14: dup
15: getfield #2 // Field sharedState:I
18: dup_x1
…
56: monitorexit //监视器退出
因为synchronzied是通过对象内部的监视器(monitor)来实现同步的。
那么关于对象内部的监视器(monitor)就值得我们做更深层次的探究了。
在java虚拟机实现规范有关monitor的描述是:每个对象有一个监视器(monitor),线程通过执行monitorenter指令尝试获取monitor的所有权,获取到以后monitor会处于锁定状态(表示被占用)。
获取monitor的所有权过程如下:
1.如果monitor的进入数为0,则该允许线程进入,然后monitor的进入数设置为1,该线程即为monitor的所有者,代表持有锁(即图中:当前对象的监视器的的Owner指向当前线程Thread-2,其他线程不是阻塞就是等待);
2.如果线程已经占有该monitor,只是重新进入,则monitor的进入数+1;
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
2.锁升级(优化部分)
在JVM底层实现锁的过程中,有三种类型的锁:偏斜锁 轻量级锁 重量级锁
优化之前(Java6 之前),synchronized的实现完全依靠操作系统内部的互斥锁,来进行用户态到内核态的切换(同步操作相当于就是采用重量级锁),极其消耗系统资源。
Java 6之后,在Oracle JDK中,JVM对synchronized进行了改进,提供了三种不同的Monitor即偏斜锁、轻量级锁、重量级锁,以此来应对不同线程环境下的锁的切换,实现了锁的升级、降级。
偏向锁 | 轻量级锁 | 重量级锁 | |
核心 | 单线程执行(未出现线程并发) | 多个线程串行(不支持并发) | 依赖OS互斥锁 |
实现机制 | 该线程在后续访问时便会自动获得锁,尽量减少不必要的轻量级锁的执行路径 | 多线程串行的访问同一个加锁对象 | 利用互斥锁实现线程之间切换,需从用户态转换到核心态,切换成本极其高。 |
性能 | 降低获取锁带来的消耗,提高性能 | 每次执行,都消耗了重复的加锁与解锁的性能开销。 | 性能最差,效率低 |
切换 | 加锁的代码有多个线程调用时,升级为轻量级锁 | 若同一时间不同线程访问同一把锁,升级为重量级锁 |
3.轻量级锁的加锁解锁
轻量级锁依赖CAS操作Mark Word来试图获取锁,如果成功,就是用普通的轻量级锁,否则,进一步升级为重量级锁。
Mark Word的结构如下:
bitfileds | 标志位(2bit位) | 状态 | 特征 | |||||
指向当前锁记录的指针 ptr to lock record | 00 | 轻量级锁 | 自旋 | |||||
hash | age | 0/1(是否偏向锁) | 01 | 无锁 | 无 | |||
Thread ID | epoch | age | 01 | 偏向锁 | 只需比较Thread ID | |||
指向重量级锁monitor的指针 ptr to heavyweight monitor | 10 | 重量级锁 | 依赖mutex(OS的互斥) | |||||
11 | 可GC | 用于标记GC |
注:lock标志位:2位二进制,锁状态标记位。
thread: 持有偏向锁的线程ID。
ptr_to_lock_record:指向栈中锁记录的指针。
1.加锁
(1) 进入同步代码块是,如果对象锁状态为无锁状态(lock 标志位“01”,biased_lock标志位 “0”),则虚拟机首先在当前线程的栈帧中创建一个Lock Record锁记录空间,用于存储锁对象目前Mark Word的拷贝。
(2)拷贝当前对象头中的Mark Word复制到Lock Record中。
(3)拷贝成功后,虚拟机尝试将
对象的Mark Word中的 当前锁记录的指针 更新为指向 Lock Record的指针,并将
线程的Lock Record中的 owner指针 指向 该对象的Mark Word 。若更新成功,继续步骤(4),否则步骤(5)。
(4)若当前线程和对象的指向都更新成功,那么代表该线程拥有了该对象的锁,并且对象的Mark Word的lock标志位设置为“00”,即表示此对象处于轻量级锁定状态。
(5)若更新失败,虚拟机首先会检查对象的Mark Word是否已经指向当前线程的栈帧。
若已指向,说明当前线程已经拥有了该对象的锁,即可直接进入同步块继续执行;
若未指向,则说明多个线程同时竞争该对象的锁,轻量级锁就要升级为重量级锁,lock标志位变为“10”,Mark Word中存储的就是是指向重量级锁的指针,后面等待锁的线程进入阻塞状态,当前线程尝试使用自旋来获取锁。(自旋:为了不让线程阻塞,而采用循环去获取锁的过程)。
2.解锁
(1)通过CAS指令,尝试把线程中复制的Mark Word(该对象的Mark Word)替换为当前的Mark Work。
(2)如果替换成功,整个同步过程即完成。
(3)如果替换失败,说明有其他线程尝试过获取该锁,锁就要升级为重量级锁,即在释放锁的同时,通知其他线程重新参与锁的竞争。