多线程编程核心技术(十一)Lock
两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
Java 语言本身提供的 synchronized 也是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在 SDK 里提供另外一种实现呢?
在 Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的 Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。
问题就是synchronized没有办法解决“破坏不可抢占条件方案”。 原因是synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。synchronized的加锁释放操作由JVM为我们进行。
如果我们重新设计一把互斥锁去解决这个问题,应该具有以下的能力
1.能够响应interrupt:如果一个线程进入了死锁的阶段是需要进行中断进行锁的释放的。
2.支持超时。如果线程一段时间无法获得到锁,返回一个错误,也释放曾持有的锁,这样也能破坏不可抢占条件
3.非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
在Lock接口中就实现了这些
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
Java Sdk里面的锁Lock保证可见性:利用了volatile相关的Happens-Before规则。获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。
class SampleLock {
volatile int state;
// 加锁
lock() {
// 省略代码无数
state = 1;
}
// 解锁
unlock() {
// 省略代码无数
state = 0;
}
}
也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。根据相关的 Happens-Before 规则:
顺序性规则:对于线程 T1,value+=1 发生在 释放锁的操作 unlock() 之前;
volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 发生在 线程 T2 的 lock() 操作 之前;
传递性规则:线程 T1 的 value+=1 发生在 线程 T2 的 lock() 操作 之前。
所以说,后续线程 T2 能够看到 value 的正确结果。
什么是可重入锁
同一个线程可以重复获取同一把锁,并且是安全的。
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock(); ②
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get(); ①
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。
推荐的三个用锁的最佳实践,它们分别是:
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
活锁问题
class Account {
private int balance;
private final Lock lock
= new ReentrantLock();
// 转账
void transfer(Account tar, int amt){
while (true) {
if(this.lock.tryLock()) {
try {
if (tar.lock.tryLock()) {
try {
this.balance -= amt;
tar.balance += amt;
} finally {
tar.lock.unlock();
}
}//if
} finally {
this.lock.unlock();
}
}//if
}//while
}//transfer
}
1.线程A给B转账,B给A转账,两个都有自己的锁,没有对象的锁
2.一段时间后释放锁,重复1步骤
Lock接口实现了互斥,Condition 实现了管程模型里面的条件变量,用来实现了同步。
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
//入队后,通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
//出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
线程之间的通信不管是syn还是lock都是对对象进行的
线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。但是不一样的是,Lock&Condition 实现的管程里只能使用前面的 await()、signal()、signalAll(),而后面的 wait()、notify()、notifyAll() 只有在 synchronized 实现的管程里才能使用。
public class demo13 {
private final Object lock = new Object();
public void add() throws InterruptedException {
synchronized (lock){
System.out.println(Thread.currentThread().getName()+" is in");
System.out.println(Thread.currentThread().getName()+" is wait");
lock.wait(2000);
lock.notifyAll();
System.out.println(Thread.currentThread().getName()+" is notify");
}
}
public static void main(String[] args) throws InterruptedException {
demo13 demo13 = new demo13();
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(() -> {
try {
demo13.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
thread.join();
}
}
}
Java 程序支持异步的两种实现方式: 1、调用方创建一个子线程,在子线程中执行方法调用,这种调用我们称为异步调用。 2、方法实现的时候,创建一个新的线程执行主要逻辑,主线程直接return,这种方法我们一般称为异步方法。

浙公网安备 33010602011771号