【java并发工具类-互斥】Lock和Condition

Lock和Condition

          • 1.比较synchronized,Lock具有的特点?
          • 2.较于synchronized,Lock如何实现可见性?
          • 3.ReentrantLock,可重入锁
          • 4.公平锁和非公平锁
          • 5.Lock等待唤醒机制—如何用两个条件变量快速实现阻塞队列?
          • 6.dubbo用等待唤醒机制实现异步转同步
          • 7.如何正确使用锁
          • 8.Lock中常见的问题

1.比较synchronized,Lock具有的特点?

Lock接口的三个方法:

// 支持中断的API
void lockInterruptibly() 
  throws InterruptedException;
// 支持超时的API,在规定的时间内尝试获得锁,如果还没有获得锁,不进入阻塞状态,直接返回。
boolean tryLock(long time, TimeUnit unit) 
  throws InterruptedException;
// 支持非阻塞获取锁的API,尝试获取锁,如果获取失败不陷入阻塞,直接返回
boolean tryLock();
2.较于synchronized,Lock如何实现可见性?

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,

3.ReentrantLock,可重入锁

什么是可重入锁?即,线程可以重复获取同一把锁。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(); // 保证锁能释放
    }
  }
}
4.公平锁和非公平锁

ReentrantLock的两个构造函数

public ReentrantLock() {//无参构造函数:默认非公平锁
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair){//根据公平策略参数创建锁
    sync = fair ? new FairSync() 
                : new NonfairSync();
}

如果我们要创建一个公平锁,就需要传入true,否则创建非公平锁,传入false,或者不传入参数调用无参构造方法即可。

那么,什么是公平锁,什么是非公平锁?

之前文章中讲过的管程模型中,锁都对应着等待队列,当线程没有获得锁时,就会进入入口等待队列中,等待唤醒线程获得锁进入方法,如果是公平锁,就会唤醒等待时间长的线程,如果是非公平锁,就有可能等待时间短的线程反而被先唤醒。

5.Lock等待唤醒机制—如何用两个条件变量快速实现阻塞队列?

直接上代码:

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原理
【java并发工具类-互斥】Lock和Condition_第1张图片

6.dubbo用等待唤醒机制实现异步转同步

当我们使用dubbo远程调用服务(RPC)时,TCP本身就是异步的 ,而RPC是使用的自定义的TCP(http调用正常的TCP会有好多信息,好多信息都是没用的),那我们平时RPC都是同步的呀,所以你会发现dubbo给你做了异步转同步的工作了。
而dubbo实现异步转同步,就是使用Lock的唤醒等待机制,等待RPC远程调用服务返回后唤醒。

7.如何正确使用锁
  • 永远只在更新对象的成员变量时加锁
  • 永远只在访问可变的成员变量时加锁(如果不加锁,可能其他线程修改了变量,读的变量可能是修改之前的)
  • 永远不在调用其他对象的方法时加锁(如果在获取当前对象锁后执行当前对象的方法,方法中调用其他对象的加锁方法,这就是死锁问题啊,同时获得两把锁。)
  • 自己看过那么多代码,加锁正确姿势,在lock后,try{}catch{}finally{unlock()}
lock();
  try{....
  }catch{...
  }finally{
    unlock();
  }
8.Lock中常见的问题
  • while(true) 总不让人省心,
    while(true)没有breadk,死循环,开发中要防止true的情况(可能你没有写true,但是有可能结果一直是true,要防止这种情况),
    除此之外,虽然Lock的tryLock是非阻塞式获得锁,没有死锁问题,但同时也需要防止活锁问题(两个账户分别给对方转账,账户A拿到自己锁,账户B也拿到自己的锁,两个账户都需要拿到对方的锁,都失败,重新拿锁,就有可能每次都是这种情况),解决活锁问题很简单,随机等待一段时间即可,下面是解决方式。
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
}
  • signalAll() 总让人省心

就像synchronized,我们最好是使用notifyall()方法,唤醒所有线程,而不是notify(),只唤醒一个线程。
同理:我们使用Lock时,唤醒线程也最好使用signAll()方法。

参考:极客时间
更多:邓新

你可能感兴趣的:(并发编程体系架构,#,java并发工具类,多线程,java,并发编程)