Android 并发编程—Semaphore

1.Semaphore信号量
Semaphore是JUC包下的一个工具类,用来管理多线程,它可以控制同时访问特定资源的线程数量,相当于限流。在多线程环境下,它通过限制执行的线程数据,保证合理使用公共资源。

使用Semaphore要达到的目的是:控制某个方法允许并发访问的线程个数。 也就是说在线程里执行某个方法的时候,在方法里用该类对象进行控制,就能保证所有的线程中最多只有指定信号量数目的该方法在执行。

Semaphore内部是基于AQS的共享模式。 它相当于给线程规定一个量,从而控制允许活动的线程数。线程需要通过acquire()方法获取许可,通过release()释放许可。如果许可数达到最大活动数后再调用acquire(),该线程便会进入等待队列,等待已获得许可的其他线程释放许可,从而使多线程能够合理的运行。

如果初始化了一个许可为1的Semaphore,那么就相当于一个不可重入的互斥锁Mutex。

2.Semaphore使用
比如开启100个线程,每个线程都调用showLog()方法,但是现在要求所有线程中,最多只能有五个线程可以同时执行该方法,其他的线程必须排队等待。此时就可以使用Semaphore进行控制。

private Semaphore semaphore;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
semaphore = new Semaphore(5);
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
showLog();
}
}).start();
}
}
private void showLog(){
try {
semaphore.acquire();
Log.i(TAG,“线程:” + Thread.currentThre ad().getName()+“执行了一个acquire请求操作”);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000); //模拟线程的耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
semaphore.release();
Log.i(TAG,“线程:” + Thread.currentThrea d().getName()+“执行了一个release请求操作”);
}

执行结果:
在这里插入图片描述

3.Semaphore原理
①构造方法
Semaphore提供了两个构造方法:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
通过permits参数​指定许可数量。默认为非公平模式。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
通过permits指定许可数量,fair参数指定是否为公平模式。

Semaphore有公平模式和非公平模式两种。
公平模式就是调用acquire()方法的顺序就是获取许可证的顺序,遵循FIFO。
非公平模式是抢占式的,也就是有可能一个新的获取线程恰好在一个许可证释放时得到了这个许可证,而前面还有等待的线程。

②获取许可
请求一个信号量,这时信号量数量减1,当减少到0的时候,下一次acquire()不会再执行,只有当执行一个release(),信号量不为0的时候才可以继续执行acquire。
从该信号量获取一个许可前,线程将一直阻塞。

Semaphore提供了如下的获取许可的方法:
public void acquire():获取一个许可,会阻塞等待其他线程释放许可;
public void acquireUninterruptibly():获取一个许可,会阻塞等待其他线程释放许可,可被中断;
public boolean tryAcquire():尝试获取许可,不会进行阻塞等待;
public void acquire(int permits):获取指定的许可数 ,会阻塞等待其他线程释放;
public void acquireUninterruptibly(int permits):获取指定的许可数,会阻塞等待其他线程释放许可,可被中断;
public boolean tryAcquire(int permits):尝试获取指定的许可数,不会阻塞等待;
public boolean tryAcquire(long timeout, TimeUnit unit):尝试获取许可,指定等待时间;
public boolean tryAcquire(int permits, long timeout, TimeUnit unit):尝试获取指定的许可数,可指定等待时间;

以acquire()和tryAcquire()两个方法为例。
(1)tryAcquire()方法
返回值表示是否获取许可成功,该方法不会阻塞等待其他线程释放许可,如果没有许可就直返返回false。
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
​sync是Semaphore的静态内部类,继承自AQS。
final int nonfairTryAcquireShared(int acquires) {
for (;😉 { //自旋
int available = getState(); //剩余的许可量
int remaining = available - acquires; //扣减需要的信号量后的值
// 信号量不足或CAS替换state失败,退出自旋
if (remaining < 0 || compareAndSetState( available, remaining))
return remaining;
}
}
这个方法用于判断当前的许可量是否充足,充足的话就用CAS修改state的值,否则退出自旋。
返回值是分配所需数量信号量后剩余的信号量,如果大于等于0说明信号量充足,小于0说明当前信号量不够。
(2)acquire方法
这个方法没有返回值,当许可不足时会阻塞线程等待其他线程释放许可。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//tryAcquireShared是模板方法,由子类实现
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
acquire()调用AQS的acquireSharedInterruptibly方法,在这个方法中用到了模板方法模式,tryAcquireShared方法是一个模板方法,需要子类去实现。
看看在Semaphore的公平和非公平模式都是如何实现该方法的。

公平模式下的tryAcquireShared方法:
protected int tryAcquireShared(int acquires) {
for (;😉 { //自旋
// 判断是否已经存在阻塞的线程
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState( available, remaining))
return remaining;
}
}

非公平模式下的tryAcquireShared方法:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;😉 { //自旋
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState( available, remaining))
return remaining;
}
}
公平和非公平模式的区别是:在公平模式的实现中会首先判断是否已经存在阻塞的线程了,如果存在阻塞线程就不会再去竞争获取许可了。

不管是公平模式还是非公平模式,tryAcquireShared()方法的返回值都是int,如果返回值大于等于0表示有足够用的信号量可用,如果返回值小于0表示当前信号量不够用了,此时会通过doAcquireSharedInterruptibly()方法把当前线程加入阻塞队列。

AQS.doAcquireSharedInterruptibly方法:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
//将当前线程添加到AQS的阻塞队列中
final Node node = addWaiter( Node.SHARED);
boolean failed = true;
try {
for (;😉 {
// 获取当前线程节点的前置节点
final Node p = node.predecessor();
if (p == head) { //前置节点是头节点,则再次尝试获取许可
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//前置节点不是头节点,则挂起当前线程,并清除无用的节点
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed) //获取许可失败,则移除节点
cancelAcquire(node);
}
}
首先创建一个分享型的node节点包装当前线程,并添加到AQS队列的尾部。
然后判断这个节点的上一个节点是不是head,如果是head就再次尝试获取锁,如果此时获取锁成功了就将刚刚那个节点设置成head;如果获取锁失败,该线程就阻塞等待。

③释放许可
释放一个许可,将其返回给信号量,这时信号量个数会增加1。

释放许可使用release()方法:
public void release() {
sync.releaseShared(1);
}
AQS中的releaseShared()方法:
public final boolean releaseShared(int arg) {
// 模板方法 需要子类实现
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
返回值表示释放许可是否成功。

Semaphore.Sync实现的tryReleaseShared()方法逻辑:
protected final boolean tryReleaseShared(int releases) {
for (;😉 { //自旋
int current = getState(); //当前的许可量
int next = current + releases; //加上需要释放的量后,总共的许可量
if (next < current) // overflow
throw new Error(“Maximum permit count exceeded”);
// CAS修改state
if (compareAndSetState(current, next))
return true;
}
}
当前线程尝试改变许可证数量,返回值表示改变许可证数量成功与否。如果改变许可证数量成功,会调用doReleaseShared方法释放阻塞的线程。

AQS.doReleaseShared()方法的逻辑:
private void doReleaseShared() {
for (;😉 { //自旋
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws = = Node.SIGNAL) {
//设置head的等待状态为0,并唤醒head.next节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
} else if (ws = = 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //成功设置为0后,将head状态设置成传播状态
continue;
}
if (h == head)
break;
}
}
doReleaseShared()方法的主要作用就是从AQS的head节点开始唤醒线程。注意,这里唤醒的是head节点的下一个节点,需要和doAcquireSharedInterruptibly()方法对应。因为doAcquireSharedInterruptibly()方法唤醒的当前节点的上一个节点,也就是head节点。

4.总结
在AQS中存在一个变量state,当创建Semaphore对象传入许可数值时,最终会赋值给state。state的数值代表同一个时刻可同时操作共享数据的线程数量,每当一个线程请求(如调用Semaphored的acquire()方法)获取同步状态成功,state的值将会减少1,直到state为0时表示已没有可用的许可数,也就是对共享数据进行操作的线程数已达到最大值,其他后来的线程将被阻塞,此时AQS内部会将线程封装成共享模式的Node结点,加入同步队列中等待并开启自旋操作。只有当持有许可证的线程执行完成任务并释放同步状态后,同步队列中的队首的结点线程才有可能获取同步状态并被唤醒执行同步操作,注意在同步队列中获取到同步状态的结点将被设置成head并清空相关线程数据(毕竟线程已在执行也就没有必要保存信息了),AQS通过这种方式便实现共享锁。
Semaphore的剩余许可量是通过AQS中的state属性进行记录的,获取许可是将该值进行减少,释放许可是将该值进行增加。当没有足够的许可时,线程会加入到阻塞队列中等待其他线程释放许可并唤醒。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值