『深入学习Java』(四) Java锁基础.md
前言
前面记录了多线程的一些基本操作,这一小节来学习 Java 中锁相关知识。
什么叫锁?
锁是一种控制多线程访问共享资源的工具。 通常,锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,并且对共享资源的所有访问都需要首先获取锁synchronized提供了对与每个对象关联的隐式监视器锁的访问。
以上是官方解释。
通俗来讲,锁可以理解成"坑位",也即一个坑位同时只能有一个人蹲,一个人蹲着其他人就得等着。
为什么需要锁?
一个模拟并发扣减库存的示例
int stockNum = 10;
@Test
public void test01() throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
Thread thread11 = new Thread(() -> {
// synchronized (this) {
if (stockNum <= 0) {
return;
}
int newStockNum = stockNum - 1;
// 模拟一些更新DB数据操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
stockNum = newStockNum;
log.info("{} 购买成功。,剩余库存:{}。", Thread.currentThread().getName(), stockNum);
// }
});
thread11.start();
}
});
thread1.start();
Thread.sleep(5000);
log.info("库存剩余:{}", stockNum);
}
执行结果
16:30:37.902 [Thread-322] INFO ThreadLockTest - Thread-322 购买成功。,剩余库存:9。
16:30:37.902 [Thread-3574] INFO ThreadLockTest - Thread-3574 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3589] INFO ThreadLockTest - Thread-3589 购买成功。,剩余库存:2。
16:30:37.901 [Thread-3598] INFO ThreadLockTest - Thread-3598 购买成功。,剩余库存:2。
16:30:37.904 [Thread-2919] INFO ThreadLockTest - Thread-2919 购买成功。,剩余库存:4。
16:30:37.902 [Thread-3602] INFO ThreadLockTest - Thread-3602 购买成功。,剩余库存:2。
16:30:37.901 [Thread-2923] INFO ThreadLockTest - Thread-2923 购买成功。,剩余库存:4。
16:30:37.902 [Thread-3578] INFO ThreadLockTest - Thread-3578 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3577] INFO ThreadLockTest - Thread-3577 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3585] INFO ThreadLockTest - Thread-3585 购买成功。,剩余库存:2。
16:30:37.901 [Thread-417] INFO ThreadLockTest - Thread-417 购买成功。,剩余库存:9。
16:30:37.901 [Thread-3597] INFO ThreadLockTest - Thread-3597 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3573] INFO ThreadLockTest - Thread-3573 购买成功。,剩余库存:2。
16:30:37.902 [Thread-3590] INFO ThreadLockTest - Thread-3590 购买成功。,剩余库存:2。
........省略
我们可以看到……库存已经卖超了。如果真实业务系统,发生大规模库存超卖情况,后果是不堪设想的。
我们将注释的 synchronized (this)
的放开,然后重新执行:
16:37:37.037 [Thread-2] INFO ThreadLockTest - Thread-2 购买成功。,剩余库存:9。
16:37:37.158 [Thread-1966] INFO ThreadLockTest - Thread-1966 购买成功。,剩余库存:8。
16:37:37.268 [Thread-1963] INFO ThreadLockTest - Thread-1963 购买成功。,剩余库存:7。
16:37:37.380 [Thread-1964] INFO ThreadLockTest - Thread-1964 购买成功。,剩余库存:6。
16:37:37.490 [Thread-1962] INFO ThreadLockTest - Thread-1962 购买成功。,剩余库存:5。
16:37:37.601 [Thread-1961] INFO ThreadLockTest - Thread-1961 购买成功。,剩余库存:4。
16:37:37.711 [Thread-1960] INFO ThreadLockTest - Thread-1960 购买成功。,剩余库存:3。
16:37:37.821 [Thread-1959] INFO ThreadLockTest - Thread-1959 购买成功。,剩余库存:2。
16:37:37.937 [Thread-1958] INFO ThreadLockTest - Thread-1958 购买成功。,剩余库存:1。
16:37:38.057 [Thread-1957] INFO ThreadLockTest - Thread-1957 购买成功。,剩余库存:0。
16:37:41.943 [main] INFO ThreadLockTest - 库存剩余:0
这就完美符合了我们的需要,当库存卖到零的时候,就不继续进行售卖了。
synchronized 的使用姿势
synchronized 使用在方法上或代码块上。
- 使用在方法上
- 普通方法,锁为对象实例。
- 静态方法,锁为对象Class。
// 普通方法。
public synchronized void test01() {
...
}
// 静态方法。
public static synchronized void test01() {
...
}
- 使用在代码中
public void test01() {
synchronized(锁对象) {
...
}
}
只有 synchronized?使用 Lock 锁
Java 中除了提供 synchronized
这种自动加解锁的特性外,还提供了可以手动加解锁的 Lock
对象。
大多数情况下,应该按照以下方式来使用:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
这么做的主要目的是,防止 l.unlock();
报错。
我们将文章开头的例子修改为 Lock
模式,也可以得到相同的结果。
Thread thread11 = new Thread(() -> {
lock.lock();
try {
if (stockNum <= 0) {
return;
}
int newStockNum = stockNum - 1;
// 模拟一些更新DB数据操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
stockNum = newStockNum;
log.info("{} 购买成功。,剩余库存:{}。", Thread.currentThread().getName(), stockNum);
} finally {
lock.unlock();
}
});
thread11.start();
Java 的 Lock 体系
Lock 与 ReadWriteLock 接口
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()
- 获取锁,并支持阻塞等待中断。即如果这个线程正在等待锁,但是被另外一个线程中断了,这个方法会抛出InterruptedException
以结束锁等待。tryLock()/tryLock(long time, TimeUnit unit)
- 尝试加锁。加锁成功返回 True,失败返回 Flase,不进行锁等待/或只进行指定时间的等待,超时后抛出InterruptedException
。unlock() - 释放锁。
newCondition() - 为锁设定"wait-sets(等待池)"。该方法可以调用多次,即设定多个等待池。
ReadWriteLock
读写锁,针对读写行为分别加锁。不过多描述了。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantLock
ReentrantLock implements Lock
。可重入的互斥锁,语义基本和synchronized
一致。
基本特性:可重入、可中断、可设定锁等待超时时间、可设置为公平锁、支持多个条件变量。
前几个基本特性,已经在上一小节描述过了。这里想着重说一下条件变量。
条件变量,就是为线程设置一个"休息室"。
当不满足条件时,线程进入"休息室"休息;满足条件时,从把线程从"休息室"喊出来。
所以与notify()/wait()
的语义是相似的。
下面的示例,模拟了一个数据缓冲区,当数据列表大小为100,开始消费。消费结束后,开始接受数据。
final Lock lock = new ReentrantLock();
final Condition addCondition = lock.newCondition();
final Condition getCondition = lock.newCondition();
public List<String> data = new ArrayList<>();
@Test
public void conditionTest() throws InterruptedException {
// 获取数据
new Thread() {
@Override
public void run() {
lock.lock();
try {
// 如果缓冲区里没有数据,开始等待
while (data.size() == 0) {
getCondition.await();
}
log.info("开始取数据。");
data.clear();
addCondition.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}.start();
// 添加数据
new Thread() {
@Override
public void run() {
lock.lock();
try {
while (data.size() == 100) {
addCondition.await();
}
log.info("开始添加数据。");
while (data.size() < 100) {
data.add("");
}
getCondition.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}.start();
}
ReentrantReadWriteLock
ReentrantReadWriteLock implements ReadWriteLock
。
可重入读写锁的基本特性与ReentrantLock
一致。也不多说了。
StampedLock
Java 8 中引入了 StampedLock 。它除了普通的锁,还支持读锁和写锁。加锁方法返回一个标志戳,用于释放锁或检查锁是否仍然有效。
- 写锁
writeLock()
会阻塞等待、独占访问。返回值可以在方法unlockWrite()
作为入参释放锁。持有写锁时,无法获得读锁,并且所有乐观读验证都会失败。 - 读锁
readLock
会阻塞等待、非独占访问,返回值可以在方法unlockRead()
作为入参释放锁。 - 乐观读锁
当未持有写锁时,
tryOptimisticRead
方法返回锁标志戳。如果获取锁标志戳后,又持有了写锁validate
方法会返回 flase。 在乐观模式下读取的字段可能非常不一致,因此仅当您对数据表示足够熟悉以检查一致性和/或重复调用方法validate()时才适用
小结
这一小节,主要对 Java 中锁的基本知识做了一些总结。
其中包含了 synchronized 、ReentrantLock、ReentrantReadWriteLock、StampedLock 的基本知识。