并发笔记(四)Lock、Condition以及Semaphore

实现管程除了synchronized之外,java SDK 并发包中提供的ReentrantLock通过实现Lock以及Condition接口实现管程模型。其中Lock实现互斥,Condition实现同步。

一.Lock实现互斥,原子性可见性

1.为什么已经有synchronized锁还要重复造轮子呢?

jdk1.6之后由于锁升级的优化synchronized的效率提升了,为什么还需要ReentrantLock呢,因为两点:破坏不可抢占条件;以及多个等待队列。

死锁的四个条件:
a.互斥(由于使用的就是互斥锁,所以这个无法破坏)
b.占有且等待(可以一次性申请全部资源)
c.不可抢占(synchronized无法破坏,因为线程申请不到资源就会进入等待队列,无法获得cpu执行权)
d.循环等待(可以按照资源的优先级申请)

ReentrantLock,对于占有部分资源的线程进一步申请其他资源的时候,如果申请不到,可以主动释放,而不是进入等待队列。那么不可抢占条件也就被破坏了。因此提供了三个方法。

响应中断的API:lockInterruptibly()

支持超时的API:boolean tryLock(long var1, TimeUnit var3)

支持非阻塞的API:boolean tryLock()

上诉三个方法都是获取锁资源的时候,如果获取失败不进入等待队列,或者是进入等待队列的被中断的,从而主动的释放当前线程持有的资源。

2.如何实现互斥的

通过AQS,AQS中有一个volatile state锁状态标志,0是代表无人占有锁资源,ReentrantLock中大于等于1代表占有锁资源,并且设置锁状态的操作是原子性的,通过CAS,CAS操作是由CPU指令保证其原子性的,也就是同一时刻只有一个线程可以通过CAS占有锁资源,保证多线程之间的互斥性。

3.如何实现可见性的

class Test {
  private final ReentrantLock lock = new ReentrantLock();
  int value;
  public void addOne() {
    // 获取锁
    lock .lock();  
    try {
      value+=1;
    } finally {
      // 保证锁能释放
      lock .unlock();
    }
  }
}

对于上诉的addOne()如何保证可见性的,可见性的分析一定是根据Java内存模型。因为AQS中的state是volatile的,并且在lock以及unlock的时候都会对state进行读写操作,由于程序的顺序性value的改变对于unlock中state是可见的,volatile的写操作对于读操作是可见的,最后根据内存模型的传递性可知,lock和unlock之间的临界区域中对于共享变量的操作是可见的。

4.ReentrantLock底层实现的原理

ReentrantLock的lock方法以及unlock的流程图

根据EMA根据管程的模型,需要等待队列以及锁资源
ReentrantLock底层使用双端队列(CLH)作为等待队列,volatile state的值代表锁资源,state=0时代表没有线程占用锁资源,>=1代表锁资源被占用。并且对于state的修改,调用Unsafe的CAS操作方法(直接操作内存,原子操作)。这样就可以保证获取锁的线程的安全性。
双端队列的添加和移除的Node也是同样的CAS操作保证等待线程的添加和删除的安全性。同时内部采用LockSupport的park以及unpark实现等待唤醒机制。
至于条件等待队列,是通过Condition结构实现,并且提供多个条件等待队列,更加的灵活。
上诉是从管程的设计模型解读。(个人理解)

可重入锁:所谓可重入锁,顾名思义,指的是线程可以重复获取同一把锁

可重入函数,指的是多个线程可以同时调用该函数,每个线程都能得到正确结果(线程安全)

公平锁和非公平锁
ReentrantLock创建的时候可以指定是否公平,公平锁是按照等待队列中的时间获取锁资源,非公平锁:等待队列中的线程有可能是等待时间短的获得锁,也有可能是其他线程竞争到锁资源,所以不公平。

用锁的最佳实践:

1.永远只在更新对象的成员变量时加锁
2.永远只在访问可变的成员变量时加锁
3.永远不在调用其他对象的方法时加锁

二.Condition实现同步

1.实现线程之间的同步,最好的案例就是使用两个等待条件实现阻塞队列。相对于synchronized的wait()、notify()、notifyAll(),ReentrantLock的await()、signal()、signalAll()语义都是一样的,都是等待唤醒机制。

2.异步转同步
使用等待唤醒机制以及回调方法,实现异步转同步。


// 创建锁与条件变量
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();
}
// 结果是否已经返回
boolean isDone() {
  return response != null;
}
// 结果返回时调用该方法   
private void doReceived(Response res) {
  lock.lock();
  try {
    response = res;
    if (done != null) {
      done.signalAll();
    }
  } finally {
    lock.unlock();
  }
}

异步线程执行完之后需要调用doReceived(),唤醒等待获取结果的线程。这样对于调用方异步调用转为同步调用就完成了,Dubbo中的RPC就是将异步调用转为同步等待获取结果的。

三.Semaphore限流器

JUC中根据不同的场景提供了很多的并发类,每个类适用的场景都不同。
Semaphore其中一个,可以用于限流器。

1.信号量模型

图片来源
在这里插入图片描述
有三部分组成:计数器,等待队列,三个原子方法。

init():初始化计数器
up():计数器加1,如果此时计数器<=0的话,唤醒等待队列一个线程并且将其从等待队列中移除。
down():计数器减1,如果此时计数器<0,当天线程阻塞进入等待队列,否则可以执行

从上诉模型中我们得出与synchronized以及lock的引用区别,管程模型可以实现的是同一时刻只有一个线程,而Semaphore可以设置同一时间多少线程同时执行临界区中的代码。

java中对应的up():release(),down():acquire()方法。

2.实现一个对象池

对象池呢,指的是一次性创建出 N 个对象,之后所有的线程重复利用这 N 个对象,当然对象在被释放前,也是不允许其他线程使用的。


class TestPool<T, R> {
  final List<T> pool;
  // 用信号量实现限流器
  final Semaphore sem;
  // 构造函数
  ObjPool(int size, T t){
    pool = new Vector<T>(){};
    for(int i=0; i<size; i++){
      pool.add(t);
    }
    sem = new Semaphore(size);
  }
  // 利用对象池的对象,调用func
  R exec(Function<T,R> func) {
    T t = null;
    sem.acquire();
    try {
      t = pool.remove(0);
      return func.apply(t);
    } finally {
      pool.add(t);
      sem.release();
    }
  }
}
// 创建对象池
ObjPool<Long, String> pool = 
  new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行  
pool.exec(t -> {
    System.out.println(t);
    return t.toString();
});

Function<T,R> 类中提供四个方法,apply(T t),compose(T t),andThen(T t),以及静态方法indentify()方法。下面的是操作。

public class Test2 {

    public static void main(String[] args) {
        Function<Integer, Integer> f1 = i -> i*4;   // lambda
        System.out.println(f1.apply(3));

        Function<Integer, Integer> f2 = i -> i+4;   // lambda
        System.out.println(f2.apply(3));

        Function<String, Integer> f3 = s -> s.length();   // lambda
        System.out.println(f3.apply("Adithya"));

        System.out.println("组合函数:"+f2.compose(f1).apply(3));
        System.out.println(f2.andThen(f1).apply(3));

        System.out.println(Function.identity().apply(10));
        System.out.println(Function.identity().apply("Adithya"));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值