Lock 与锁机制
5.9.2 Lock接口
基本信息
Package java.util.concurrent.locks
public interface Lock
用于控制对共享资源的访问。通常情况下 Lock 仅允许一个线程来访问共享资源,但也有一些特殊的实现类允许并发访问。Lock 加解锁和 synchronized 有相同的内存语义,下一个线程加锁后可以看到前一个线程解锁前所有操作。
由于 synchronized 具有以下缺点,我们需要 Lock 来强化功能:
- 效率低。锁释放情况少,获得锁不能设置超时或者中断。
- 不够灵活。加减锁的时机单一,每个锁只有单一条件。
- 无法知道是否成功获取到了锁。
分类
根据线程是否锁住同步资源可以分为:
- 悲观锁、互斥同步锁(锁)
- 缺点:阻塞和唤醒带来性能劣势;可能造成永久阻塞;可能造成优先级反转。
- 优点:就算临界区持锁时间越来越差,对锁的开销影响不大
- 认为如果不对资源进行上锁,就可能会有其他线程争抢而造成数据错误。所以每次获取和修改数据时都先把资源锁住。
- 适合并发写入多的情况;临界区持锁时间长的情况。例如:临界区有 IO 操作;临界区代码复杂循环量大;临界区竞争激烈。
- 例子:
synchronizedLock
- 乐观锁、非互斥同步锁(不锁)
- 缺点:如果自旋时间长或者不停重试,会造成消耗的资源越来越多。
- 认为在线程处理数据时不会有其他线程干扰而不提前对资源锁定。在更新数据时对比修改期间是否有其他线程改动,如果没有则正常操作;如果发现数据不一致则选择放弃、报错、重试等策略。
- 一般利用 CAS 算法实现。
- 适合并发写入少,大部分操作是读取的场景。不加锁可以让读的性能大幅提高。
- 例子:原子类、并发容器
根据多线程能否公用一把锁可以分为:
- 共享锁(可)
- 独占锁(不可)
根据多线程竞争是否需要排队可以分为:
- 公平锁(排队)
优点:每个线程公平处理,在等待一段时间后总有执行机会。
缺点:慢,吞吐量小。 - 非公平锁(先插队,失败再排队)
优点:避免唤醒期间的空档期而提高效率。
缺点:可能产生线程饥饿(长时间内始终得不到执行)。
根据同一个线程是否可以重复获得同一把锁可以分为:
- 可重入锁
- 优点:避免死锁;提高封装性。
- 不可重入锁
根据是否可以中断可分为:
- 可中断锁
- 非可中断锁
根据等待锁的过程可以分为:
- 自旋锁(自旋)
当同步代码内容简单,线程状态转换时间开销比代码执行时间还要长,设计出自旋锁。
让线程状态不变的前提下进行自旋,如果自旋完成后之前锁定的资源已经释放锁,则不必切换状态直接获取资源,避免了切换线程状态的开销。
缺点:如果锁被占用时间过长,自旋线程会浪费处理器资源。虽然自旋开销低于悲观锁,但随着自旋时间的增增长开销也线性增长。
适用于多核处理器的服务器,并发程度不是特别高,临界区小。 - 非自旋锁(阻塞)
在没有拿到锁的情况下直接阻塞线程。
重要方法
lock()
获取锁。如果锁被其他线程获取则等待。
它不会在遇到异常时自动释放锁,所以我们一般要求在上锁后把业务用try-finally包裹,并在finally中释放锁。lock()不能被中断,如果死锁发生则可能永久等待。tryLock()
尝试获取锁,如果当前锁没有被其他线程占用则成功获取,返回true,否则返回false.
可以根据是否能获取到锁来决定后续程序行为。
方法会立刻返回,不会在拿不到锁时等待(无视公平策略)。tryLock(long time, TimeUnit unit)
带有超时时间的tryLock(). 在等待设定时间内拿不到锁则放弃。lockInterruptibly()
相当于等待时间无限长的tryLock(), 但是它可以被中断。unlock()
解锁。
具体实现类ReentrantLock 类
- 默认为非公平策略,可设置为公平策略
- 互斥锁、可重入锁、可中断锁
- 类似
synchronized机制,但更加灵活 - 利用 AQS 算法实现
isHeldByCurrentThread()查看锁是否被当前线程持有getQueueLength()返回正在等待当前锁的队列长度
ReentrantReadWriteLock 类
- 使用时允许多个线程同时使用统一资源,但只允许一个线程对资源进行写操作
- 读期间不允许进行写操作,写期间不允许进行读操作
- 读锁为共享锁,写锁为独享锁
- 适用于读多写少的场景
- 插队策略
设置公平策略:private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
不公平策略避免解饿:写锁可以随时尝试插队。读线程可以插队,但需要在等待队列头结点的线程不是写线程时可以插队。 - 升降级策略
支持降级但不支持升级。若支持升级容易操作死锁,因为假设两个写线程想升级,都需要对方先释放锁,造成死锁。
java
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.writeLock = reentrantReadWriteLock.writeLock();锁优化
JVM 对锁进行了优化:
- 自旋锁的自适应:在自旋尝试一段时间后自动转为阻塞所来防止资源消耗过大。
- 锁消除:对于无需加锁但是加锁的场景,消除掉锁。
- 锁粗化:频繁对一些资源加锁解锁时,合并相邻的操作进入一个锁中。
使用锁时的启发规则:
- 缩小同步代码块
- 尽量不要锁定方法
- 减少锁使用次数
- 避免人为制造热点数据
- 避免锁中包含锁
- 选择适合的锁和工具类