Fun with concurrency: using Semaphore to achieve current limiting

Insert picture description here

use

In real life, we often encounter current restrictions. For example, a movie theater has only 2 doors, so only 2 people's tickets can be checked at the same time each time. And Semaphore is used in Java to control the number of threads that access specific resources at the same time

public class SemaphoreUseDemo {
    
    

    public static void main(String[] args) {
    
    
        ExecutorService service = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 8; i++) {
    
    
            final int num = i;
            Runnable runnable = () -> {
    
    
                try {
    
    
                    semaphore.acquire();
                    System.out.println("no " + num + " check");
                    TimeUnit.SECONDS.sleep((long) Math.random() * 200);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    semaphore.release();
                    System.out.println("---- " + "no " + num + " finish");
                }
            };
            service.execute(runnable);
        }
    }
}
no 0 check
no 1 check
---- no 1 finish
no 2 check
---- no 2 finish
---- no 0 finish
no 3 check
---- no 3 finish
no 4 check
---- no 4 finish
no 6 check
---- no 6 finish
no 5 check
no 7 check
---- no 7 finish
---- no 5 finish

It can be seen that at most 2 people can check the ticket at the same time each time, and only one person can check the ticket of the next person after the ticket check.

Semaphore is a shared lock implemented based on AQS, which is a thread synchronization tool class in Java

Semaphore will define the total amount of permits of resources when it is used. This permits will be set to the state in the AQS class. State has different meanings in different tool classes. The meaning in Semaphore is as follows

When state>0, you can acquire the lock and set state-1. When state=0, the thread will be blocked and wait for other threads to release the lock. When the lock is released, state+1, so that other threads can get the lock again.

When permits is defined as 1, Semaphore is equivalent to a mutex

Source code analysis

It is recommended to look first. I will not introduce the various APIs related to AQS in
"Playing with Concurrency: What are the functions of AQS?" "
Playing with Concurrency: How does CountDownLatch control the order of concurrent execution of threads?"

Semaphore's constructor has the following two types, permits specify the number of resources, fair is true and fair lock, otherwise it is unfair lock, the default is unfair lock, and the throughput is high

public Semaphore(int permits) {
    
    
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    
    
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

Insert picture description here
Based on AbstractQueuedSynchronizer, Sync has done some encapsulation for Semaphore, such as setting the number of resources, obtaining the number of resources, and the release of shared locks

The difference between FairSync and NonfairSync is relatively simple, but there is a difference in the way of trying to acquire the lock (tryAcquireShared)

acquire resources

public void acquire() throws InterruptedException {
    
    
    sync.acquireSharedInterruptibly(1);
}

Just call the shared lock that responds to the interrupt in AQS, and the subclass can rewrite the logic of trying to acquire the lock (tryAcquireShared)

Let's take a look at what is the difference between the logic of a fair lock and an unfair lock trying to acquire the lock?

Fair lock

// Semaphore.FairSync#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;
    }
}

Unfair lock

The logic of an unfair lock trying to acquire a lock will eventually call the following method

// Semaphore.Sync#nonfairTryAcquireShared
final int nonfairTryAcquireShared(int acquires) {
    
    
    for (;;) {
    
    
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

It can be seen that it is not queued first like a fair lock, but directly tries to acquire the lock

FairSync: Every time you must queue to acquire the lock.
NonfairSync: First use CAS to acquire the lock. When the lock cannot be acquired, then queue to acquire the lock.

releaseRelease resources

// Semaphore
public void release() {
    
    
    sync.releaseShared(1);
}
// AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {
    
    
    if (tryReleaseShared(arg)) {
    
    
    	// 释放成功,唤醒阻塞队列中的线程
        doReleaseShared();
        return true;
    }
    return false;
}

Subclass overrides the logic that attempts to release the lock

// 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");
        if (compareAndSetState(current, next))
            return true;
    }
}

Use cas to add state value +1

Reference blog

[1]https://mp.weixin.qq.com/s/ic1lX1G3kYvmztTgN0Yihg

Guess you like

Origin blog.csdn.net/zzti_erlie/article/details/113902910