Lock接口的三个方法:
// 支持中断的API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的API,在规定的时间内尝试获得锁,如果还没有获得锁,不进入阻塞状态,直接返回。
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的API,尝试获取锁,如果获取失败不陷入阻塞,直接返回
boolean tryLock();
java SDK经典Lock使用的经典实例,就是try{}catch{}
class X {
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
java里多线程的可见性是Happend-before规则保证的,而synchronized之所以可以保证可见性,是因为存在监视器规则:synchronized的的解锁Happend-before于后续对这个锁的加锁。那么Java SDK中的Lock如何保证可见性呢?
其实它是利用了volatile相关的happend-before规则,java SDK中的ReentrantLock,内部持有volatile变量state,获取锁的时候读写state,解锁的时候也会读写volatile修饰的state。
也就是说在加锁时,读写volatile的值,那么之前其他线程对共享变量的修改对当前线程t1可见的,
然后根据顺序性规则,线程t1对value进行+=1Happend-before于线程t1的解锁
最后线程t1解锁,又读写了volatile修饰的state,
什么是可重入锁?即,线程可以重复获取同一把锁。ReentrantLock翻译过来就是可重入锁。
在addOne方法中获得锁,进入方法中再执行get方法,就又会加锁,如果是可重入锁,当前线程是可以再次加锁成功的,如果不可重入,就会陷入阻塞。
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(); // 保证锁能释放
}
}
}
ReentrantLock的两个构造函数
public ReentrantLock() {//无参构造函数:默认非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair){//根据公平策略参数创建锁
sync = fair ? new FairSync()
: new NonfairSync();
}
如果我们要创建一个公平锁,就需要传入true,否则创建非公平锁,传入false,或者不传入参数调用无参构造方法即可。
那么,什么是公平锁,什么是非公平锁?
之前文章中讲过的管程模型中,锁都对应着等待队列,当线程没有获得锁时,就会进入入口等待队列中,等待唤醒线程获得锁进入方法,如果是公平锁,就会唤醒等待时间长的线程,如果是非公平锁,就有可能等待时间短的线程反而被先唤醒。
直接上代码:
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 (队列已满){ //这里为什么需要用while循环?
notFull.await(); // 等待队列不满
}
// 省略入队操作...
notEmpty.signal(); //入队后,通知可出队
}finally {
lock.unlock();
}
}
void deq(){ // 出队
lock.lock();
try {
while (队列已空){
notEmpty.await(); // 等待队列不空
}
// 省略出队操作...
notFull.signal(); //出队后,通知可入队
}finally {
lock.unlock();
}
}
}
代码中为什么await()需要用while循环?
1.当线程wait()被唤醒,再次执行时是从下一行代码开始执行的,对啊?难道不应该从下一行开始执行的么?
2.从下一行开始执行没有错,但是如果当前条件变量可能还不满足,依然需要wait()怎么办呢?所以需要while循环,
这就设计到了管程模型,管程模型包括,入口等待队列,共享变量,条件变量和条件变量队列,wait(),notify,notifyall()三个方法,当没有拿到锁对象的在入口等待队列等待拿到锁,拿到锁对象的,访问方法中不满足条件变量,wait(),把当前线程放入条件变量等待队列中,等待唤醒,释放锁对象,当条件满足时,可以条件condition.notifyall(),也就是不会立即执行唤醒的线程,而是先执行完当前线程,从条件变量等待队列中放到入口等待队列中,所以在wait()的线程再次执行的时候,可能条件变量又发生变化了!
详细请看管程模型:synchronized原理
当我们使用dubbo远程调用服务(RPC)时,TCP本身就是异步的 ,而RPC是使用的自定义的TCP(http调用正常的TCP会有好多信息,好多信息都是没用的),那我们平时RPC都是同步的呀,所以你会发现dubbo给你做了异步转同步的工作了。
而dubbo实现异步转同步,就是使用Lock的唤醒等待机制,等待RPC远程调用服务返回后唤醒。
lock();
try{....
}catch{...
}finally{
unlock();
}
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;
break; //新增:退出循环
} finally {
tar.lock.unlock();//记得break也要释放锁。
}
}//if
} finally {
this.lock.unlock();
}
}//if
Thread.sleep(随机时间); //新增:sleep一个随机时间避免活锁
}//while
}//transfer
}
就像synchronized,我们最好是使用notifyall()方法,唤醒所有线程,而不是notify(),只唤醒一个线程。
同理:我们使用Lock时,唤醒线程也最好使用signAll()方法。
参考:极客时间
更多:邓新