目录
编辑
锁相关
公平锁和非公平锁?
可重入锁
锁的升级
乐观锁和悲观锁
版本号机制
CAS 算法
乐观锁有哪些问题?
锁相关
公平锁和非公平锁?
-
公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
-
非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
可重入锁
也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
锁的升级
四种状态:无锁、偏向锁、轻量级锁、重量级锁
-
偏向锁:偏向锁是为了减少无竞争情况下锁的性能开销而引入的一种优化措施。当一个线程访问一个同步块时,如果没有竞争,那么JVM会自动将锁的对象头Mark Word中的线程ID指向该线程,并将锁状态标志设置为偏向锁。这样,该线程再次进入同步块时,可以直接获取锁,而无需再次进行同步操作。偏向锁的状态转换非常快速,因为它只需要更新Mark Word中的线程ID即可。
对象头由两部分组成,第一部分(Mark Word)存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一个额外字段用来记录数组长度。
-
轻量级锁:当多个线程竞争同一个锁时,偏向锁就会失效,此时JVM会将锁的状态标志设置为轻量级锁。轻量级锁的实现是基于CAS操作和自旋锁。当一个线程获取轻量级锁时,JVM会将锁对象的Mark Word复制一份到线程的栈帧中,并将锁对象的Mark Word指向线程的栈帧。这时,其他线程再次竞争该锁时,会进入自旋状态,不断尝试获取锁。如果自旋超过一定次数,或者其他线程已经持有该锁,那么轻量级锁就会升级为重量级锁。
-
重量级锁:如果锁的竞争非常激烈,或者自旋超过一定次数,那么轻量级锁就会升级为重量级锁。重量级锁的实现是基于操作系统的互斥量或信号量。当一个线程获取重量级锁时,JVM会将该线程挂起(阻塞),直到锁被释放。重量级锁的状态转换非常耗时,因为需要涉及到用户态和内核态之间的切换,所以应该尽量避免锁的升级到重量级锁。
synchronized锁升级不可逆的原因是由于锁的升级过程涉及到锁对象的内部结构和状态的改变,而这些改变是不可逆的。重量级锁状态涉及到操作系统级别的资源分配和线程阻塞,无法直接回退到轻量级锁或偏向锁状态。
乐观锁和悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
版本号机制
一般是在数据表中加上一个数据版本号 version
字段,表示数据被修改的次数。当数据被修改时,version
值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version
值,在提交更新时,如果刚才读取到的 version 值为当前数据库中的 version
值相等时才更新,否则重试更新操作,直到更新成功。
CAS 算法
CAS 的全称是 Compare And Swap(比较与交换) ,用来实现乐观锁。CAS 的思想就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
CAS 涉及到三个操作数:
-
V:要更新的变量值(Var)
-
E:预期值(Expected)
-
N:拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
乐观锁有哪些问题?
1、ABA 问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,这种情况并不能说明它的值没有被其他线程修改过,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
解决:在变量前面追加上版本号或者时间戳。
2、循环时间长开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:
可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。
3、只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
但是从 JDK 1.5 开始,提供了
AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference
类把多个共享变量合并成一个共享变量来操作。