java共享锁 Semaphore与ReentrantReadWriteLock

1.什么是共享锁

其实在我的上一篇文章里面已经说乐什么是独占锁,就是只能被一个线程拥有。那在这里,共享锁就是可以同时被多个线程所拥有。

2.我们要讲的那些共享锁

2.1 ReentrantReadWriteLock

四级踩点过的我一直不知道 Reentrant,然后我去百度了。以下是百度来的 Reentrant。

reentrant
英 [riːˈɛntrənt] 美 [ˌriˈɛntrənt]
可重入;可重入的;重入;可再入的;重进入

由此可见,ReentrantReadWriteLock字面意思是可重入写读锁。
而ReentrantReadWriteLock是一个实现类,他继承了ReadWriteLock

2.1.1关于ReadWriteLock

ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。

读锁:它是“共享锁”,能同时被多个线程获取。
写入锁:它是“独占锁”,写入锁只能被一个线程锁获取。
里面提供了两个方法。

// 返回用于读取操作的锁。
Lock readLock()
// 返回用于写入操作的锁。
Lock writeLock()

2.2.1 AQS 抽象队列同步器

  • AQS分析

在这里插入图片描述在走进ReentrantReadWriteLock 我们发现他的成员变量里面有Sysc。再走进去我们发现他是继承了AbstractQueuedSynchronizer这个类(抽象队列同步器)。,是一套多线程访问共享资源的同步器框架。好像网上都把他简写为AQS,那我也先这样称呼。

我门接下来稍稍走进AQS吧。
AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。
在这里插入图片描述
 它维护了一个volatile int state(代表共享资源),该字段用来描述有多少线程获持有锁独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数)在共享锁中state就是持有锁的数量。但是在 ReentrantReadWriteLock 中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位 表示读锁状态(读锁个数),低16位表示写锁状态
在这里插入图片描述

和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:

getState()
setState()
compareAndSetState()

CLH队列不是一个实在的队列而是一个逻辑双向队列。它将每一个请求资源封装成CLH的一个节点,仅在节点中存储关系。
在这里插入图片描述
这是NODE一个节点的部分信息。我们可以看到他存储了下一个和上一个节点和当前线程。他就是通过这种存储来实现这个虚拟队列。

而AQS作为一个顶层的接口,他提供了如下几个方法去让使用者去继承。

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。调用期间可能会不断的阻塞然后解除阻塞,直到调用tryAcquire成功
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
除非调用tryAcquireShared成功,其它表现和acquire相同。

  • 继承了AQS的实例分析

实现方式:(同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。)

1.独占锁ReentrantLock(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire() 独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。
ps:获取多少次锁就要释放多少次锁,保证state是能回到零态的。
2.CountDownLatch:任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。
3.ReentrantReadWriteLock:里面有两个锁,读锁和写锁。读锁和写锁的锁主体都是 Sync ,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是可重入独占锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。
在获取写锁的代码里面tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:
必须确保写锁的操作对读锁可见,如果允许读锁在已被获取 的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

  • 对于AQS的总结:在我看来,不论是ReentrantLock还是ReentrantReadWriteLock,都内置了内部类去继承AQS,内部类里重写各种获取资源释放资源的方法,来被这些类引用形成自己的锁的方式(独占可重入,或者共享等等)。归根都是对于AQS

作者絮絮叨叨:感觉这个模型和之前的管程模型有点像,有兴趣的小伙伴可以去搜搜我之前的文章。并发编程-初级之认识并发编程

2.2 Semaphore与限流

1.什么是Semaphore

Semaphore,现在普遍翻译为“信号量”,以前也曾被翻译成“信号灯”,因为类似现实生活里的红绿灯,车辆能不能通行,要看是不是绿灯。同样,在编程世界里,线程能不能执行,也要看信号量是不是允许。
在这里插入图片描述这三个方法详细的语义具体如下所示。
init():设置计数器的初始值。
down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当
前线程可以继续执行。
up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个
线程,并将其从等待队列中移除。

在java里面是这么说的

1.Semaphore是一个计数信号量(本质是一个共享锁)
2.从概念上将,Semaphore包含一组许可证。
3.如果有需要的话,每个acquire()(down)方法都会阻塞,直到获取一个可用的许可证。
4.每个release()(up)方法都会释放持有许可证的线程,并且归还Semaphore一个可用的许可证。
5.然而,实际上并没有真实的许可证对象供线程使用,Semaphore只是对可用的数量进行管理维护。
Semaphore类图

在这里插入图片描述从图中可以看出:
(01) 和"ReentrantLock"一样,Semaphore也包含了sync对象,sync是Sync类型;而且,Sync是一个继承于AQS的抽象类。
(02) Sync包括两个子类:“公平信号量"FairSync 和 “非公平信号量"NonfairSync。sync是"FairSync的实例”,或者"NonfairSync的实例”;默认情况下,sync是NonfairSync(即,默认是非公平信号量)。

2.与Lock区别:允许多个线程访问同一个临界区(共享锁)

现实案例:各种连接池

代码案例:(对象池)

class ObjPool<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();
});

我们用一个 List来保存对象实例,用 Semaphore 实现限流器。关键的代码是 ObjPool 里
面的 exec() 方法,这个方法里面实现了限流的功能。在这个方法里面,我们首先调用
acquire() 方法(与之匹配的是在 finally 里面调用 release() 方法),假设对象池的大小是
10,信号量的计数器初始化为 10,那么前 10 个线程调用 acquire() 方法,都能继续执
行,相当于通过了信号灯,而其他线程则会阻塞在 acquire() 方法上。对于通过信号灯的线
程,我们为每个线程分配了一个对象 t(这个分配工作是通过 pool.remove(0) 实现的),
分配完之后会执行一个回调函数 func,而函数的参数正是前面分配的对象 t ;执行完回调
函数之后,它们就会释放对象(这个释放工作是通过 pool.add(t) 实现的),同时调用
release() 方法来更新信号量的计数器。如果此时信号量里计数器的值小于等于 0,那么说
明有线程在等待,此时会自动唤醒等待的线程。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值