显示锁
相对应的内置锁(synchronized)相比, 显示锁更加灵活, 完全基于Java语言实现, 更加方便使用者自定义扩展和使用.
具有一些内置锁不具备的功能:
- 限时抢占, 指定时间内加锁失败即返回, 不需要像synchronized一样阻塞线程.
- 可中断抢锁, 其他线程给抢锁线程发送中断信号, 抢锁线程能够响应并停止抢占.
- 多个等待队列, 为同一个锁的不同类型加锁线程设置不同的等待队列, 例如生产者消费者模式中的生产者队列, 和消费者队列.
显示锁还能解决内置锁中重量级锁内核态和用户态频繁切换引入的性能开销问题.
JDK1.5引入java.util.concurrent 并发包, 其中LOCK接口, 显示锁接口, 其实现类对象称为: 显示锁对象. 同时在这个包下还有很多其他并发工具类.
显示锁Lock接口
抽象方法
public interface Lock {
void lock();// 抢占锁, 失败会阻塞线程
void lockInterruptibly() throws InterruptedException;// 可中断抢锁, 可以响应中断信号.
boolean tryLock();// 非阻塞式抢占锁, 成功true, 失败false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 超时抢占锁, 可通过具体时间单位支持任意粒度的时间单位.
void unlock();// 释放锁
Condition newCondition();// 获取和显示锁绑定的Condition对象. 用于等待-通知模式的线程间通信.
}
从Lock抽象接口看出Lock支持的场景:
- 可中断, 可响应中断信号.
- 支持非阻塞式的加锁.
- 支持限时抢锁.
ReentrantLock
lock接口的基础实现版本, 实现了Lock接口的所有功能, 基于AQS实现, 在竞争激烈场景下效率高于内置锁 . AQS重点.
Feature
可重入: 同一个线程加锁成功后, 可以多次重复加锁.
互斥(独占): 一个锁同一时刻只能被一个线程拥有. 其他线程只能等owner释放.
可重入示例:
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantTest(reentrantLock);
}
public static void reentrantTest(Lock lock) {
lock.lock();
System.out.println("加锁成功1");
lock.lock();
System.out.println("加锁成功2");
lock.unlock();
lock.unlock();
}
}
## 输出
加锁成功1
加锁成功2
互斥特性演示:
public class ReentrantLockDemo {
private static int sum = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
add(reentrantLock);
}
public static void add(Lock lock) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(10);
IntStream.rangeClosed(1, 10).forEach(i->{
countDownLatch.countDown();
executorService.submit(() -> increment(lock));
});
countDownLatch.await();
executorService.shutdown();
while (!executorService.isTerminated()) {
}
System.out.println(sum);
}
public static void increment(Lock lock) {
lock.lock();
System.out.println(Thread.currentThread().getName());
IntStream.rangeClosed(1, 100).forEach(i->{
sum++;
});
lock.unlock();
}
}
## 输出
pool-1-thread-1
pool-1-thread-7
pool-1-thread-2
pool-1-thread-4
pool-1-thread-3
pool-1-thread-5
pool-1-thread-6
pool-1-thread-8
pool-1-thread-9
pool-1-thread-10
1000
这里结果无论尝试多少次结果都是1000, 没有线程安全问题, 因为reentrantLock是互斥锁, 保证同一时刻只能有一个线程能执行 sum++ 操作.
显示锁使用模板
lock()抢占锁
public static void increment(Lock lock) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName());
IntStream.rangeClosed(1, 100).forEach(i->{
sum++;
});
}finally {
lock.unlock();
}
}
注意事项
- unlock操作必须在finally中执行, 确保锁会被释放.
- lock必须在try操作之外, try没有声明的抛出异常, lock不一定成功, 失败不需要执行unlock操作, 未持有锁的情况下释放锁会报错.
- lock和try之间没有任何代码, 避免抛出异常导致释放锁得不到执行.
tryLock()非阻塞式加锁
public static void increment2(Lock lock) {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName());
IntStream.rangeClosed(1, 100).forEach(i->{
sum++;
});
}finally {
lock.unlock();
}
}
}
- tryLock操作放在条件中执行,会返回true/false 成功后才执行临界区代码.
tryLock(long time, TimeUnit unit)超时加锁
public static void increment3(Lock lock) throws InterruptedException {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName());
IntStream.rangeClosed(1, 100).forEach(i->{
sum++;
});
}finally {
lock.unlock();
}
}
}
类似tryLock(), 只是在失败时会等待一定时间.
基于显示锁实现”等待-通知”方式的线程间通信
在Java内置锁中, 等待-通知模式的线程间通信通过Object对象的wait和notify实现, 在juc中有类似的组件也能实现相同的功能. Condition
Condition接口的抽象方法
// 语义等同于Object.wait(), 使得当前线程加入await()等待队列, 并释放当前锁. 其他线程调用signal()时,等待队列中的某个线程会被唤醒, 重新抢锁.
void await() throws InterruptedException;
// 唤醒一个等待队列中的线程, 效果等同于Object.notify()
void signal();
// 唤醒等待队列中的所有线程, 等同于Object.notifyAll()
void signalAll()
// 语义等同于await, 超时后会终止等待.
boolean await(long time, TimeUnit unit) throws InterruptedException;
由于Condition对象是基于显示锁的, 所以不能单独创建一个Condition对象, 需要借助显示锁实例去获取其绑定的Condition对象, 一个Lock实例可以有任意数量的Condition对象, 可以通过lock.newCondition()方法显示获取一个和当前锁对象绑定的Condition对象实例.
等待通知示例
public static void main(String[] args) throws InterruptedException {
Thread waitThread = new Thread(new WaitWorker());
Thread notifyThread = new Thread(new NotifyWorker());
waitThread.start();
Thread.sleep(1000);
notifyThread.start();
waitThread.join();
notifyThread.join();
}
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
static class WaitWorker implements Runnable{
@Override
public void run() {
lock.lock();
try{
print("Wait Thread Locked, I will call await.");
condition.await();
print("Wait Thread Locked again.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class NotifyWorker implements Runnable{
@Override
public void run() {
lock.lock();
try{
print("notify thread locked, I will call the signal");
condition.signal();
print("called the notify, But i have not release the Lock");
}finally {
lock.unlock();
}
}
}
## 输出
Wait Thread Locked, I will call await.
notify thread locked, I will call the signal
called the notify, But i have not release the Lock
Wait Thread Locked again.
💡 await(long time, TimeUnit unit) 进入等待后, 在超时之前可以提前被signal唤醒, 然后继续执行.
LockSupport
JUC提供的线程阻塞唤醒工具类. 支持线程在任意位置暂停/唤醒当前线程.

方法主要分为两类: park让线程暂停, unpak唤醒暂停的线程.
带时间参数表示截止时间或者一定的时长约束, parkNanos 和 parkUntil的区别是前者表示时长, 比如: 5s,后者表示到某个时间点: 比如: 2022-01-17 18:18:18 这个截止时间, 只是表示形式为时间戳.
使用示例
public class LockSupportDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new parkWorker(), "t1");
Thread t2 = new Thread(new parkWorker(), "t2");
t1.start();
Thread.sleep(1000);
t2.start();
t1.interrupt();
LockSupport.unpark(t2);
}
static class parkWorker implements Runnable {
@Override
public void run() {
Util.print(Thread.currentThread().getName() + " 即将调用LockSupport.park方法, 然后进入无限等待");
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
Util.print(Thread.currentThread().getName() + " 被中断了, 继续执行");
} else {
Util.print(Thread.currentThread().getName() + " 被唤醒了, 继续执行");
}
}
}
}
## 输出
t1 即将调用LockSupport.park方法, 然后进入无限等待
t2 即将调用LockSupport.park方法, 然后进入无限等待
t1 被中断了, 继续执行
t2 被唤醒了, 继续执行
LockSupport和Thread.sleep()区别
- sleep无法从外部唤醒, lockSupport可以, unpark().
- sleep方法声明了中断异常 InterrupteException异常, 是一个受检异常, 需要捕获后者再抛出这个异常. locksupport不需要.
- 调用线程的interrupt()方法. 都可以设置中断标识, 唤醒线程, 两者的响应方式不一致, sleep会抛出异常, locksupport不会抛出异常.
- locksupport控制的更加精准, 更灵活, 可以唤醒指定的线程.
- locksupport可以设置blocker对象, 方便用来监视.
- 两者都不会释放锁资源
LockSupport和Object.wait()区别
- wait需要在synchronized代码块中执行. locksupport任意位置.
- wait在interrupt时会抛出异常, locksupport不会.
- notify必须在wait之后, unpark不必在park之后.
显示锁的分类
可重入锁和不可重入锁
可重入表示一个线程在持有一个对象锁时, 重复去加多次锁, 允许就是可重入, 只能一次就是不可重入, ReentrantLock标准可重入实现类.
乐观锁和悲观锁
根据线程在进入临界区前是否锁住资源来区分
悲观锁每次进入临界区之前都会加锁, 锁住同步资源, 其他线程就会被阻塞. 适用于写多读少的场景, 在并发写场景下性能比较高.
乐观锁不会加锁, 但是在读和用的时候都会关注数据的版本, 判断数据是否被修改过, 如果修改过就重新读取再使用, 在低并发场景下非常高效. 在Java中乐观锁基本都是基于CAS自旋实现的. synchronized 轻量级锁就是乐观锁, 重量级锁是悲观锁. JUC中基于抽象队列同步器实现的锁都是乐观锁.
公平锁和非公平锁
表示线程的加锁顺序和抢占锁的机会是否公平, 在时间上公平锁分配策略一定是优先分配给等待时间更长的线程, 满足FIFO队列. 非公平就打破了这种公平性, 例如Synchronized的重量级锁, 队列EntryList中的线程可能比新线程晚拿到对象锁. 因为每个线程在进入EntryList之前会先尝试直接加锁. 默认ReentrantLock也是非公平锁, 可以通过参数控制其是否公平.
可中断锁和不可中断锁
线程在等待加锁时, 等待一段时间后放弃等待. 就是可中断锁, 不可中断锁就是申请加锁会陷入无限等待直到加锁成功, ReentrantLock的tryLock(long)就是可中断锁. synchronized就是不可中断锁, ReentrantLock的lock方法也是不可中断锁.
共享锁和独占锁
共享锁就是可以有多个线程同时获取锁, 独占锁就是同一时刻只能有一个线程拿到锁资源. Synchronized就是独占锁, ReentrantLock也是独占锁. 共享锁允许多个线程同时获得锁, 例如: ReentrantReadWriteLock 类是一个共享锁实现类. 它支持多个线程一起读, 写操作还是只能一个线程操作. 相对于ReentrantLock来说ReentrantReadWriteLock在读场景比较多的场景下效率更高.