Java 锁机制详解(三)Lock
简介
Lock
以更强大灵活的方式,作为了 synchronized
锁的替代品。
相比较 synchronized
,Lock
有如下优势:
- 可以尝试获取锁,线程不必一直等待;
- 可以判断锁状态;
- 支持公平锁。
- 可以通过读锁、写锁提升锁效率。
- ...
功能
1、Lock
Lock
接口源码有如下方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
这些方法的用途如下。
方法 | 用途 |
---|---|
lock() |
获取锁,锁不可用则线程休眠。 |
lockInterruptibly() |
同 lock(),区别是可以响应中断。 |
tryLock() |
尝试获取锁,锁可用则获取后立即返回 true,不可用则立即返回 false。 |
tryLock(long time, TimeUnit unit) |
限定时间内尝试获取锁。有俩种情况返回 false:1.响应中断;2.超时。 |
unlock() |
释放锁。 |
newCondition() |
用于 Lock 加锁线程的等待和唤醒操作,后面会详细说明。 |
2、ReentrantLock
ReentrantLock
是排他锁,即同一时刻只允许一个线程访问。
ReentrantLock
的方法有很多,详情可以参考官方文档,这里不一一介绍了。
利用 ReentrantLock
可以实现如下简单加锁:
Lock lock = new ReentrantLock();
private void method() {
lock.lock();
try {
// do sth
} finally {
lock.unlock();
}
}
公平锁和非公平锁
顾名思义,公平锁即线程执行按照先进先出的原则。
在 ReentrantLock
构造函数中传入 true
以启用公平锁。
3、Condition
类似于 synchronized
配合 wait()/notify()/notifyAll()
实现等待唤醒,Lock
依赖 Condition
实现上述操作。
相比 synchronized
,Lock
支持创建多个 Condition
,从而可以根据需要按组将线程等待或唤醒(即可以唤醒指定线程),更加灵活。
4、代码示例
synchronized
一节中,我们利用 synchronized
关键字实现了俩线程交替执行、多线程顺序执行,所以这里将利用 Lock
实现这俩个功能。
1、俩线程交替执行。
直接上代码。
private static class InTurnThread extends Thread {
private Lock lock;
private Condition condition;
public InTurnThread(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
super.run();
lock.lock();
try {
// 交替运行
while (true) {
// todo sth 你的业务逻辑
condition.signalAll();
Thread.sleep(1000); //这里为了方便测试 暂停1s
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
执行:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Thread inTurnThreadA = new InTurnThread(lock, condition);
Thread inTurnThreadB = new InTurnThread(lock, condition);
inTurnThreadA.start();
inTurnThreadB.start();
2、多线程顺序执行
照旧。
private static class OrderThread extends Thread {
private Lock lock;
private Condition condition;
private OrderThread next;
public OrderThread(Lock lock, OrderThread next) {
this.lock = lock;
this.condition = lock.newCondition();
this.next = next;
}
@Override
public void run() {
super.run();
lock.lock();
try {
while (true) {
// todo sth 你的业务逻辑
next.condition.signal();
Thread.sleep(1000); // 为了方便测试 暂停1s
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void setNext(OrderThread next) {
this.next = next;
}
}
乍一看没什么问题,但是上述代码是错误的。
分析一下,假如现在有 A、B、C 三个线程,它的执行顺序可能是怎样的。
- A 启动,执行
next.condition.signal()
时,因为第二个线程还没有进入锁并await()
,所以无效。此时 B、C 都有机会先获得锁。 - 假如 C 先获得了锁,则执行
next.condition.signal()
时,因为 A 已经await()
,所以此时 A 被唤醒。此时 A、B 都有机会获得锁执行。 - 继续,假如 B 运气好,终于轮到它获得锁了,此时 B 执行
next.condition.signal()
,C 被唤醒。此时 A(上次没执行)、C 都有机会获得锁执行。
到这儿相信都看出来了,总有俩个线程处于抢占锁状态,顺序不确定。究其原因,就是因为线程初次获取锁时,顺序随机,导致错误的唤醒了线程。
Java 锁机制详解(一)synchronized 一节讲过顺序执行线程,它是如何避免问题的呢?
回顾下代码:
private class OrderInTurnThread extends Thread {
private int order;
public OrderInTurnThread(int order) {
this.order = order;
}
@Override
public void run() {
super.run();
synchronized (lock) {
while (!isStop) {
// 符合条件 执行后 唤醒其它所有等待线程
if (flag % THREAD_COUNT == order) {
msg.in(order + "");
flag++;
lock.notifyAll();
}
// 不符合条件 或 符合条件执行完毕 进入等待 交给下个线程执行
try {
Thread.sleep(500);
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notifyAll();
}
msg.in(order + " end");
}
}
可以看到,虽然 lock.notifyAll()
唤醒了全部线程,但是错误的线程即使抢占到了锁,也会立即休眠。
所以 Lock
也可以参考类似实现,下面的例子以 ArrayList
添加次序为序,依次执行线程。
private static class OrderInTurnThread extends Thread {
private Lock lock;
private Condition condition;
private OrderManager manager;
public OrderInTurnThread(Lock lock, OrderManager manager) {
this.lock = lock;
this.condition = lock.newCondition();
this.manager = manager;
}
@Override
public void run() {
super.run();
lock.lock();
try {
while (true) {
// 判断是不是轮到当前线程执行
if (manager.isCurThreadOrder(this)) {
//todo sth 你的业务逻辑
manager.next().condition.signal();
Thread.sleep(1000); // 为了方便测试 暂停1s
}
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
private static class OrderManager {
private List<OrderInTurnThread> threads = new ArrayList<>();
private int curIndex = 0;
private OrderInTurnThread curThread;
public void add(OrderInTurnThread thread) {
threads.add(thread);
}
/**
* 找到顺序添加的下一个OrderInTurnThread
*/
public OrderInTurnThread next() {
curThread = threads.get(++curIndex % threads.size());
return curThread;
}
/**
* 遍历开启线程
*/
public void start() {
if (threads.isEmpty()) {
return;
}
curThread = threads.get(curIndex);
for (OrderInTurnThread thread : threads) {
thread.start();
}
}
/**
* 是否轮到当前线程执行
*/
public boolean isCurThreadOrder(OrderInTurnThread thread) {
if (curThread == thread) {
return true;
}
return false;
}
}
执行:
OrderManager manager = new OrderManager();
OrderInTurnThread threadA = new OrderInTurnThread(lock, manager);
threadA.setName("A");
OrderInTurnThread threadB = new OrderInTurnThread(lock, manager);
threadB.setName("B");
OrderInTurnThread threadC = new OrderInTurnThread(lock, manager);
threadC.setName("C");
manager.add(threadA);
manager.add(threadB);
manager.add(threadC);
manager.start();
// 执行顺序为 A、B、C、A、B、C...
现在来分析下修改后的代码,它的执行顺序可能是怎样的:
- A 先启动,执行
next.condition.signal()
时,因为第二个线程还没有进入锁并await()
,所以无效。此时 B、C 都有机会先获得锁,且新的目标线程为 B。 - 假如 C 先获得了锁,在判断是否是目标线程时未通过,所以直接休眠。此时只有 B 能获取锁。
- 至此顺序执行。
因为限制了序列所对应的线程,所以即使错误的线程先行执行,也会直接休眠。而且相比较 synchronized
,Lock
在顺序正式建立起来后,只会唤醒下一个线程,比 notifyAll()
全部唤醒效率更高。
上面只是个例子,代码不够安全健壮请自行忽略...
5、ReentrantReadWriteLock
上面说到 ReentrantLock
是排他锁,同一时刻只允许一个线程访问。
但是存在这种情况:大多数场景下多线程都在读取数据,只有少数场景在写。如果多线程都在读数据时使用排他锁,对于读效率是很低且没有必要的。
ReentrantReadWriteLock
则解决了这个问题。它的原则如下:
- 多个读锁不互斥;
- 读锁写锁互斥;
- 多个写锁互斥;
即允许同时读,但只能有一方写。
ReentrantReadWriteLock
读写锁的使用和 ReentrantLock
类似,比较简单,不赘述了。
总结
Lock 讲的很粗糙,主要是因为现在 synchronized 效率上与 Lock 并无二致,使用 Lock 多数在于一些特殊场景(需要响应中断、或者公平锁的场景),所以只简单阐述,没有深入细节。
[TOC]