Lock( 互斥锁ReentrantLock)和Condition
One. lock接口
1.synchronized与Lock
前提:
synchronized:1.5以前Sy性能不行,1.6进行了优化。但是他还存在一个致命的问题。如果没有申请到资源,线程会直接阻塞。也释放不了已经占有的资源,非常容易发生死锁。
所以才有了Lock()
- 能够响应中断 synchronized的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么
线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞
状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能
够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了
void lockInterruptibly() throws InterruptedException;
- 支持超时 如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个
错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线
程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
boolean tryLock();
2.可重入锁
线程可以重复获取一把锁
我们通过一个例子来看可重复锁
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock(); //2
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get(); //1
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
上面的代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在
① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入
的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻
塞。
3.可重入函数
可重入函数,指的是多个线程可以同时调用该函数,每个线程都能得到正确结果;同时在一个线程内支持线程切换,无论被切换多少次,结果都是正确的。多线程可以同时执行,还支持线程切换,所以,可重入函数是线程安全的
既然我们说可重入函数是线程安全的,那么他和一般线程安全的函数有什么区别呢
3.1 可重入函数与线程安全的区别和联系
- 可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据
- 线程安全是在多个线程情况下引发的,而可重入函数可以在只有一个线程的情况下来说。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
- !如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。(和普通线程安全的方法区别)
- 线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响使结果是相同的。
4.公平锁与非公平锁
在Lock的实现类ReentrantLock里,我们发现有两个构造函数。一个是
无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传
入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁
// 无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据公平策略参数创建锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync()
: new NonfairSync();
}
这个公平锁的公平,用在我们去唤醒其他等待线程的时候。
如果是传入true,即公平锁,判断谁等待时间长,就唤醒。
如果是非公平,则随机。
4.Lock实现类:独占锁ReentrantLock
ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。也就是说他只能被一个线程重复获取。
他也分为公平锁和非公平锁。
5.用锁的最佳实践
众所周知,非到性能问题,一般不用锁,因为风险性太高,那么我们真的要用到锁的时候可以遵循下面的原则
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
这三条规则,前两条估计你一定会认同,最后一条你可能会觉得过于严苛。但是我还是倾向于你去遵守,因为调用其他对象的方法,实在是太不安全了,也许“其他”方法里面有线程
sleep() 的调用,也可能会有奇慢无比的 I/O 操作,这些都会严重影响性能。更可怕的是,“其他”类的方法可能也会加锁,然后双重加锁就可能导致死锁。
Two. Condition条件变量
1.优化后管程的实现:管程相当于对于受保护资源的并发访问的实现方式
synchronized只支持一个条件变量,所以Java进行了优化,支持多个条件变量。
比如一个阻塞队列就需要两个条件变量
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();
}
}
}
不过,这里你需要注意,Lock 和 Condition 实现的管程,线程等待和通知需要调用
await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。
但是不一样的是,Lock&Condition 实现的管程里只能使用前面的 await()、signal()、
signalAll(),而后面的 wait()、notify()、notifyAll() 只有在 synchronized 实现的管程里才
能使用。如果一不小心在 Lock&Condition 实现的管程里调用了 wait()、notify()、
notifyAll(),那程序可就彻底玩儿完了。
2.Condition和ReentrantLock结合(相当于加上等待与唤醒机制)
2.1 Condition 与wait,notify 与 notifyAll 唤醒的区别
- 相同点:1.都需要获取锁
- 相同点: 2.功能基本相同
- 异常 :1.前者对应的是ReentrantLock这样子的锁。后者对应的是synchronized 。
- 异常: 2.synchronized只能由一个唤醒等待(因为只有一个条件变量),但是ReentrantLock里面可以有多个Condition
2.2例子:库存问题
1.没有锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// LockTest2.java
// 仓库
class Depot {
private int size; // 仓库的实际数量
private Lock lock; // 独占锁
public Depot() {
this.size = 0;
this.lock = new ReentrantLock();
}
public void produce(int val) {
// lock.lock();
// try {
size += val;
System.out.printf("%s produce(%d) --> size=%d\n",
Thread.currentThread().getName(), val, size);
// } catch (InterruptedException e) {
// } finally {
// lock.unlock();
// }
}
public void consume(int val) {
// lock.lock();
// try {
size -= val;
System.out.printf("%s consume(%d) <-- size=%d\n",
Thread.currentThread().getName(), val, size);
// } finally {
// lock.unlock();
// }
}
};
// 生产者
class Producer {
private Depot depot;
public Producer(Depot depot) {
this.depot = depot;
}
// 消费产品:新建一个线程向仓库中生产产品。
public void produce(final int val) {
new Thread() {
public void run() {
depot.produce(val);
}
}.start();
}
}
// 消费者
class Customer {
private Depot depot;
public Customer(Depot depot) {
this.depot = depot;
}
// 消费产品:新建一个线程从仓库中消费产品。
public void consume(final int val) {
new Thread() {
public void run() {
depot.consume(val);
}
}.start();
}
}
public class LockTest2 {
public static void main(String[] args) {
Depot mDepot = new Depot();
Producer mPro = new Producer(mDepot);
Customer mCus = new Customer(mDepot);
mPro.produce(60);
mPro.produce(120);
mCus.consume(90);
mCus.consume(150);
mPro.produce(110);
}
}
结果完全和我们想的不一样
2.加上独占锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// LockTest2.java
// 仓库
class Depot {
private int size; // 仓库的实际数量
private Lock lock; // 独占锁
public Depot() {
this.size = 0;
this.lock = new ReentrantLock();
}
public void produce(int val) {
// lock.lock();
// try {
size += val;
System.out.printf("%s produce(%d) --> size=%d\n",
Thread.currentThread().getName(), val, size);
// } catch (InterruptedException e) {
// } finally {
// lock.unlock();
// }
}
public void consume(int val) {
// lock.lock();
// try {
size -= val;
System.out.printf("%s consume(%d) <-- size=%d\n",
Thread.currentThread().getName(), val, size);
// } finally {
// lock.unlock();
// }
}
};
// 生产者
class Producer {
private Depot depot;
public Producer(Depot depot) {
this.depot = depot;
}
// 消费产品:新建一个线程向仓库中生产产品。
public void produce(final int val) {
new Thread() {
public void run() {
depot.produce(val);
}
}.start();
}
}
// 消费者
class Customer {
private Depot depot;
public Customer(Depot depot) {
this.depot = depot;
}
// 消费产品:新建一个线程从仓库中消费产品。
public void consume(final int val) {
new Thread() {
public void run() {
depot.consume(val);
}
}.start();
}
}
public class LockTest2 {
public static void main(String[] args) {
Depot mDepot = new Depot();
Producer mPro = new Producer(mDepot);
Customer mCus = new Customer(mDepot);
mPro.produce(60);
mPro.produce(120);
mCus.consume(90);
mCus.consume(150);
mPro.produce(110);
}
}
这个结果就和我们想象的一样了。单仅仅是这样不满足现实的开发。库存是不能为负的所以我们要在修改条件的时候加上条件。这个时候就要用上Condition
独占锁和条件变量
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
// LockTest3.java
// 仓库
class Depot {
private int capacity; // 仓库的容量
private int size; // 仓库的实际数量
private Lock lock; // 独占锁
private Condition fullCondtion; // 生产条件
private Condition emptyCondtion; // 消费条件
public Depot(int capacity) {
this.capacity = capacity;
this.size = 0;
this.lock = new ReentrantLock();
this.fullCondtion = lock.newCondition();
this.emptyCondtion = lock.newCondition();
}
public void produce(int val) {
lock.lock();
try {
// left 表示“想要生产的数量”(有可能生产量太多,需多此生产)
int left = val;
while (left > 0) {
// 库存已满时,等待“消费者”消费产品。
while (size >= capacity)
fullCondtion.await();
// 获取“实际生产的数量”(即库存中新增的数量)
// 如果“库存”+“想要生产的数量”>“总的容量”,则“实际增量”=“总的容量”-“当前容量”。(此时填满仓库)
// 否则“实际增量”=“想要生产的数量”
int inc = (size+left)>capacity ? (capacity-size) : left;
size += inc;
left -= inc;
System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n",
Thread.currentThread().getName(), val, left, inc, size);
// 通知“消费者”可以消费了。
emptyCondtion.signal();
}
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
}
public void consume(int val) {
lock.lock();
try {
// left 表示“客户要消费数量”(有可能消费量太大,库存不够,需多此消费)
int left = val;
while (left > 0) {
// 库存为0时,等待“生产者”生产产品。
while (size <= 0)
emptyCondtion.await();
// 获取“实际消费的数量”(即库存中实际减少的数量)
// 如果“库存”<“客户要消费的数量”,则“实际消费量”=“库存”;
// 否则,“实际消费量”=“客户要消费的数量”。
int dec = (size<left) ? size : left;
size -= dec;
left -= dec;
System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n",
Thread.currentThread().getName(), val, left, dec, size);
fullCondtion.signal();
}
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
}
public String toString() {
return "capacity:"+capacity+", actual size:"+size;
}
};
// 生产者
class Producer {
private Depot depot;
public Producer(Depot depot) {
this.depot = depot;
}
// 消费产品:新建一个线程向仓库中生产产品。
public void produce(final int val) {
new Thread() {
public void run() {
depot.produce(val);
}
}.start();
}
}
// 消费者
class Customer {
private Depot depot;
public Customer(Depot depot) {
this.depot = depot;
}
// 消费产品:新建一个线程从仓库中消费产品。
public void consume(final int val) {
new Thread() {
public void run() {
depot.consume(val);
}
}.start();
}
}
public class LockTest3 {
public static void main(String[] args) {
Depot mDepot = new Depot(100);
Producer mPro = new Producer(mDepot);
Customer mCus = new Customer(mDepot);
mPro.produce(60);
mPro.produce(120);
mCus.consume(90);
mCus.consume(150);
mPro.produce(110);
}
}
Three.Duboo中Lock 和 Condition的使用
1.怎样实现异步?
1.调用方创建子线程,在子线程执行方法调用
2.方法实现时创造一个新的线程执行逻辑,主线程return
2.源码分析
首先我们要知道RPC调用大多数是异步的。发送完RPC请求的时候,线程是不会等待RPC的相应结果。
但我们的常理知道我们用rpc却是同步的。这中间就是Dubbo帮我们同步转了异步。
public class DubboInvoker{
Result doInvoke(Invocation inv){
// 下面这行就是源码中 108 行
// 为了便于展示,做了修改
return currentClient
.request(inv, timeout)
.get();
}
}
原理很简单:在接到返回结果之前阻塞调用线程,让其等待,当返回结果之后唤醒,执行任务。
// 创建锁与条件变量
private final Lock lock
= new ReentrantLock();
private final Condition done
= lock.newCondition();
// 调用方通过该方法等待结果
Object get(int timeout){
long start = System.nanoTime();
lock.lock();
try {
while (!isDone()) {
done.await(timeout);
long cur=System.nanoTime();
if (isDone() ||
cur-start > timeout){
break;
}
}
} finally {
lock.unlock();
}
if (!isDone()) {
throw new TimeoutException();
}
return returnFromResponse();
}
// RPC 结果是否已经返回
boolean isDone() {
return response != null;
}
// RPC 结果返回时调用该方法
private void doReceived(Response res) {
lock.lock();
try {
response = res;
if (done != null) {
done.signal();
}
} finally {
lock.unlock();
}
}