1. 锁的基础知识:
1.1 锁的种类:
-
乐观锁
乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对数据锁定(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般是通过加版本号然后进行比对的方式实现);
特点:乐观锁是一种并发类型的锁,本身不对数据进行加锁通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,这种方式大大的提高了并发数据请求的性能。
例如:Java JUC中的atomic包就是乐观锁的一种实现。如 AtomicInteger 通过CAS(Compare And Set)操作实现线程安全的自增;
-
悲观锁
悲观锁是基于一种悲观的态度来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人才可对数据进行加锁,然后才可以对数据进行操作。
特点:可以完全保证操作数据的独占性和正确性,但因其加锁释放锁的过程会造成消耗,所以性能不高;
例如:synchronized 就属于悲观锁,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据;
1.2 对象头:
-
Mark Word
用于存储对象的运行时数据,如:HashCode、GC分代年龄、锁状态标志等;
-
Class Metadata Address(对象指向它的类元数据指针的地址)
虚拟机通过指针来确定这个对象是哪个类的实例;
-
Array Length(数组的长度)
仅对象是一个数组时记录,因为虚拟机可以通过java对象的元数据信息确定java对象的大小,但是从数组的元数据中却无法确定数组的大小;
Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下:
25 bit | 4bit | 1bit 是否是偏向锁 | 2bit 锁标志位 | |
---|---|---|---|---|
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
1.3 锁的状态:
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中,以32位的JDK为例:
2. 介绍锁
2.1 为什么会有这么多锁的状态?
经过Synchronized保证线程安全的原理课程介绍,我们知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。到JDK1.6版本,HotSpot虚拟机开发团队在这个版本实现了各种锁优化技术,如:适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(LightWeight Locking)和偏向锁(Biased Locking)等,用来解决线程间高效的共享数据,和解决竞争问题,从而提高程序的效率。
2.3 轻量级锁(乐观锁):
-
轻量级锁的作用:
轻量级锁并不是用来替代重量级锁的,而是假设没有锁竞争的前提下(乐观锁),轻量级锁使用CAS操作避免了使用互斥量的开销;但是如果存在锁竞争,除了互斥量的开销外,还额外发生CAS操作,所以在有竞争的情况下,轻量级会比传统的重量级更慢(所以会膨胀为重量级锁);
-
轻量级锁的加锁过程
1).在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock record)的空间,用于存储锁对象目前的mark word的拷贝(图13-3中Lock record的displaced hdr为拷贝值);
2).虚拟机将使用CAS操作尝试将对象的mark word更新为指向Lock record的指针,并将Lock record中的owner指向对象的mark word(表示这个线程就拥有了该对象的锁);如果更新成功,则执行步骤(3),否则执行步骤(4)。
3).如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象mark word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态(如图13-4);
4).如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则,说明多个线程竞争锁,这时需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。自旋达到一定次数CAS操作依然没有成功,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
-
轻量级锁的解锁过程:
1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
2)如果替换成功,整个同步过程就完成了。
3) 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程
2.4 偏向锁(乐观锁):
-
偏向锁的作用:
它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步消除掉,连CAS操作都不做了。如果在获得锁的线程执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步;
-
偏向锁加锁过程(假设当前虚拟机启用了偏向锁,即启用参数-XX:+UseBiasedLocking):
1).当锁对象第一次被线程获取的时候,虚拟机会把对象头中的偏向锁的标识设置成1,锁标志位为01,,即偏向模式;如果不是第一次,访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
3).如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则使用CAS操作把获取到这个锁的线程ID记录到对象的mark word中,然后执行(5);如果竞争失败,执行(4)。
4).如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
5).执行同步代码。
-
偏向锁解锁过程:
1).当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束,根据锁对象是否处于被锁定的状态,撤销偏向后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态;
2).后续的如上面介绍的轻量级锁那样执行;
2.2 重量级锁:
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
整体的锁状态升级流程如下:
综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
2.5 偏向锁、轻量级锁的状态转化及对象Mark Word的关系
应用场景:
- 偏向锁:只有一个线程进入临界区;
- 轻量级锁:多个线程交替进入临界区;
- 重量级锁:多个线程同时进入临界区。
3.锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 追求响应时间,锁占用时间很短 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,锁占用时间较长 |
参考: