锁是用来控制多个线程访问共享资源的方式,悲观锁能够防止多个线程同时访问共享资源,乐观锁可以允许多个线程并发的访问共享资源,但是只能有一个进行修改。
在 Lock 接口出现以前,Java 程序是靠 synchronized 关键字实现锁功能的,而 jdk 5之后,并发包中新增了 Lock 接口以及相关实现类来实现锁的功能,它提供了与 synchronized 关键字类似的同步功能。只是在使用的时候,需要显示地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性。
使用 synchronized 关键字将会隐式的获取锁,但是它将锁的获取和释放固化了,也即是先获取,再释放。当然这种方式简化了锁的管理,可是扩展性没有显示的获取锁和释放来的好。例如,针对一个场景,手把手进行锁的获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不是那么容易实现了,而使用 Lock 确容易许多。
Lock 接口的特性
Lock 接口提供的 synchronized 关键字所不具备的主要特性
-
尝试非阻塞地获取锁
尝试获取锁,获取成功返回 true,获取失败返回 false
-
能被中断的获取锁
与 synchronized 不同,阻塞队列中的线程可被中断,中断异常将会被抛出
-
超时获取锁
在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回
Lock 接口的 API
Lock 是一个接口,它定义了锁获取和释放的基本操作,其 API 如下:
// 阻塞式获取锁,调用该方法当前线程会获取锁,当锁获得后,该方法返回
void lock();
// 可中断的获取锁,和lock方法不同之处在于该方法会响应中断
void lockInterruptibly() throws InterruptedException;
// 尝试非阻塞获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false
boolean tryLock();
// 超时获取锁,当前线程在以下三种情况下会被返回:
// 当前线程在超时时间内获取了锁
// 当前线程在超时时间内被中断
// 超时时间结束,返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 获取等待通知组件,该组件和当前的锁绑定
// 当前线程只有获取了锁,才能调用该组件的await方法,而调用后,当前线程将释放锁
Condition newCondition();
Lock 接口的实现类
ReentrantLock:可重入锁
ReentrantReadWriteLock:读写锁
jdk1.7 ConcurrentHashMap 中的 Segment 继承了 ReentrantLock
Lock是 jdk 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。
使用方法:
多线程下访问(互斥)共享资源时, 访问前加锁,
访问结束以后解锁,解锁的操作推荐放入 finally 块中。
// 根据不同的实现 Lock 接口类的构造函数得到一个锁对象
Lock lock = ...;
// 获取锁位于 try 块的外面
lock.lock();
try {
// 访问共享资源
} finally {
lock.unlock();
}
注意:
加锁位于对资源访问的 try 块的外部,特别是使用 lockInterruptibly 方法加锁时就必须要这样做,这为了防止线程在获取锁时被中断,这时就不能释放锁。
实现 Lock 接口的基本思想
实现锁的功能,需要两个必备元素:
- 一个是表示锁状态的变量(假设0表示没有线程获取锁,1表示已有线程占有锁),该变量必须声明为 volatile 类型;
- 一个是队列,队列中的节点表示因未能获取锁而阻塞的线程
为了解决多核处理器下多线程缓存不一致的问题,表示状态的变量必须声明为voaltile类型,并且对表示状态的变量和队列的某些操作要保证原子性和可见性。原子性和可见性的操作主要通过 Atomic 包中的方法实现。
线程获取锁的大致过程,不考虑可重入、中断或超时
-
读取表示锁状态的变量
-
如果表示状态的变量的值为0,那么当前线程尝试将变量值设置为1,通过CAS操作完成
-
若成功,表示获取了锁,如果该线程已位于在等待锁的队列中,则将其出列,并将下一个节点则变成了队列的头节点
-
如果该线程未入列,则不用对队列进行维护
-
然后当前线程从 lock 方法中返回,对共享资源进行访问
-
若失败,则当前线程将自身放入等待锁的队列中并阻塞自身,此时线程一直被阻塞在 lock 方法中,没有从该方法中返回(被唤醒后仍然在 lock 方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)
-
-
如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞(被唤醒后仍然在 lock 方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)
注意:唤醒并不表示线程能立刻运行,而是表示线程处于就绪状态,仅仅是可以运行而已
线程释放锁的大致过程
- 释放锁的线程将状态变量的值从1设置为0,并唤醒等待(锁)队列中的队首节点,释放锁的线程从就从 unlock 方法中返回,继续执行线程后面的代码
- 被唤醒的线程(队列中的队首节点)和可能和未进入队列并且准备获取锁的线程竞争锁,重复获取锁的过程
注意:可能有多个线程同时竞争去获取锁,但是一次只能有一个线程去释放锁,队列中的节点都需要它的前一个节点将其唤醒,例如有队列 A、B、C ,即由A释放锁时唤醒B,B释放锁时唤醒C