在多线程在获取资源的时候,锁用于保证资源获取的有序性和占用形,可以控制多个线程访问共享资源的顺序。Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对 java 主流的锁进行分类,梳理锁相关的基础知识。

一、乐观锁和悲观锁

乐观锁和悲观锁从看待并发问题的不同角度延伸出来的锁概念。

1.1 乐观锁

乐观的看待并发问题,认为数据不会被修改,所以不对数据上锁,只是在更新的时候判断一下在此期间数据有没有被更新。常见的乐观锁实现方式有“数据版本机制”或“CAS操作”。

1.1.1 数据版本机制

在表中进行更新数据时,先给数据表加一个版本字段,每成功操作一次记录,记录的版本号+1。
先查询那条记录,获取版本字段,更新时判断此刻版本字段的值是否与刚刚查询出来的值相等。
相等说明这段时间没有其他程序对其进行操作,可以执行更新,将版本字段的值加1。
不相等则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。

SQL 示例:

// 查询出用户信息(取得version)
select (status,version) from user where id=#{id}

// 修改用户status为2
update user set status=2,version=version+1 where id=#{id} and version=#{version};

1.1.2 CAS 操作

CAS, compare and swap的缩写,中文翻译成比较并交换,CAS机制也被用于锁的设计,该机制实现的锁也被称为自旋锁。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。

CAS 是 java.util.concurrent 并发包的核心,在 javaReentrantLock 锁也是利用对 CAS 进行循环调用来实现加锁。

1.2 悲观锁

悲观的看待并发问题,认为自己操作数据的时候其他线程总是会修改数据,所以每次在拿数据的时候都会上锁,这样其他线程想拿或者修改这个数据就会阻塞直到它拿到锁。典型的例子就是 Jdk 的 synchronized 关键字。

二、排他锁和共享锁

排他锁和共享锁是锁的实现方案,具体的锁是对该方案的实现。

排他锁(X锁)也叫写锁、独占锁,是指该锁一次只能被一个线程锁持有。如果线程T对对象A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排他锁的线程既能读数据又能修改数据。

共享锁(S锁)也叫读锁,可以被多个线程持有,如果对象A被加上共享锁后,其他线程就不能对对象A再加排他锁,但是可以再加共享锁,获得共享锁的线程只能读数据不能修改数据。

三、互斥锁和读写锁

互斥锁和读写锁是对排他锁和共享锁的具体实现,其中互斥锁和读写锁中的写锁对应着排他锁,读锁对应着共享锁。

互斥锁的具体实现有 synchronizedReentrantLockReentrantLockJDK1.5 的新特性,采用 ReentrantLock 可以替代替换 synchronized 传统的锁机制,更加灵活。

读写锁的具体实现就是读写锁 ReadWriteLock

四、可重入锁

可重入锁又名递归锁,重入锁机制用于避免死锁问题。对于同一个线程在外层方法获取锁的时候,在进入内层方法时也会自动获取锁,避免访问方法时出现该方法已经被自己锁定而导致死锁的情况。在 Java 中并发包提供的 synchronizedReentrantLockReentrantReadWriteLock 都是可重入锁。

五、公平锁和非公平锁

公平锁多个线程相互竞争时要排队,多个线程按照申请锁的顺序来获取锁。

非公平锁多个线程相互竞争时,先尝试插队,插队失败再排队,例如:synchronizedReentrantLock

六、分段锁

分段锁是一种锁的设计方法,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作,细化了锁的粒度以减少冲突,从而提升性能。CurrentHashMap 底层就用了分段锁,使用 Segment,就可以进行并发使用了。

七、锁升级

锁升级源于对 synchronized 的优化,在这之前 synchronized 被称为重量级锁,JDK 1.6 为了减少获得锁和释放锁所带来的性能消耗,在 JDK 1.6 里引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁。当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。当线程竞争更激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。