信号量Semaphore深入解读

 

1 简介

Semaphore可翻译为信号量,它维护一组许可证, 每次尝试获取许可证时都将阻塞等待直到可获取,它才能获取到并解除阻塞状态。 Semaphore可以控制一些物理或逻辑资源的访问或使用,它常常用于限制线程数目。在实际开发中,可用作流量控制,特别对于一些公共资源有限的应用场景,如数据库连接,或是一些其他限流的缓存池。(基于JDK1.8)

2 示例

这是一个使用信号量控制对缓存池中items访问的示例。

public class SemaphoreDemo {
    static class Pool {
        private static final int MAX_AVAILABLE = 6;
        private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
        public Object getItem() throws InterruptedException {
            available.acquire();
            return getNextAvailableItem();
        }
        public void putItem(Object x) {
            if (markAsUnused(x))
                available.release();
        }
        protected Object[] items;
        protected boolean[] used = new boolean[MAX_AVAILABLE];
        Pool() {
            items = new Object[15];
            for (int i = 0; i < items.length; i++) {
                items[i] = "item" + i;
            }
        }
        protected synchronized Object getNextAvailableItem() {
            for (int i = 0; i < MAX_AVAILABLE; ++i) {
                if (!used[i]) {
                    used[i] = true;
                    return items[i];
                }
            }
            return null; // not reached
        }
        protected synchronized boolean markAsUnused(Object item) {
            for (int i = 0; i < MAX_AVAILABLE; ++i) {
                if (item == items[i]) {
                    if (used[i]) {
                        used[i] = false;
                        return true;
                    } else
                        return false;
                }
            }
            return false;
        }
    }
    public static void main(String[] args) {
        final int THREAD_COUNT = 10;
        ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
        Pool pool = new Pool();
        for (int i = 0; i < THREAD_COUNT; i++) {
            int tmpI = i;
            threadPool.submit(() -> {
                try {
                    Object item = pool.getItem();
                    System.out.printf("当前线程:%s,获取到缓存池中的资源:%s\n", Thread.currentThread().getName(), item);
                   Thread.sleep(7);
                   pool.putItem(item);
                   System.out.printf("当前线程:%s,已将缓存池中的资源%s放回池中\n", Thread.currentThread().getName(), item);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        threadPool.shutdown();
    }
}
SemaphoreDemo

 

假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接.

class SemaphoreTest {
    private static final int THREAD_COUNT = 30;
    private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
    private static Semaphore s = new Semaphore(10);
​
    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        s.acquire();
                        System.out.println("save data");
                        s.release();
                    } catch (InterruptedException e) {
​
                    }
                }
            });
        }
        threadPool.shutdown();
    }
}

3 实现原理

Semaphore内部主要有一个Sync类型成员变量sync, Sync是继承抽象类AbstractQueuedSynchronizer的静态抽象内部类。

Semaphore利用父类AQS实现了一个共享锁,Sync有两个子类NonfairSyncFairSync ,分另代表非公平锁、公平锁。共享锁的关键在于实现重写tryAcquireSharedtryReleaseShared 方法,这两个方法分别会被父类的模板方法acquireSharedreleaseShared 所调用,具体细节请看AbstractQueuedSynchronizer实现原理分析

Semaphore的默认构造方法使用非公平锁,Semaphore的构造方法有一个布尔型可选参数fair,此参数指定锁的公平锁。

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

(1) 静态内部类Sync

构造方法Sync(int)将父类AbstractQueuedSynchronizer的实例变量state设置为指定的许可证数permits

Sync(int permits) {
    setState(permits);//
}

getPermits()`返回的许可证数即是父类AQS的state值.

final int getPermits() {
    return getState();
}

 

nonfairTryAcquireShared是非公平锁尝试释获取锁的方法,每成功获取一次锁,就从池中拿走一个许可证,而剩余的许可证就少1个。

其主要逻辑是: CAS自旋直到成功将state减1,并返回新的state,或当前已获取到的许可证数超出了设定的许可证总数,方法返回。

此方法返回负数时,线程将被阻塞。

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;//acquires一般是1
        if (remaining < 0 ||//为负数,表示超出了设定的许可证总数,直接返回。不能再获取许可证
            compareAndSetState(available, remaining))
            return remaining;
    }
}

 

公平锁与非公平锁释放锁状态的逻辑是一样的,都会执行tryReleaseShared方法。每释放一把锁,就将一个许可证放回池中,可用的许可证就多一个。

其主要逻辑是: CAS自旋直到成功将state加1,最终返回true.

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;//acquires一般是1
        if (next < current) // overflow   next成为负数,next超出int类型的最大可表示范围
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

 

reducePermits减少许可证数,逻辑与nonfairTryAcquireShared类似,其逻辑是:CAS自旋尝试将state减少指定数目reductions

final void reducePermits(int reductions) {
    for (;;) {
        int current = getState();
        int next = current - reductions;
        if (next > current) // underflow
            throw new Error("Permit count underflow");
        if (compareAndSetState(current, next))
            return;
    }
}

 

drainPermits将所有许可证拿走,其逻辑是:CAS自旋尝试将state设为0,并返回当前所有可用的许可证。

final int drainPermits() {
    for (;;) {
        int current = getState();
        if (current == 0 || compareAndSetState(current, 0))
            return current;
    }
}

 

非公平的Sync:NonfairSync

NonfairSync代表一个公平锁的实现,它并没有自己的逻辑,其tryAcquireShared方法也是直接调用父类的nonfairTryAcquireShared方法。

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -2694183684443567898L;
​
    NonfairSync(int permits) {
        super(permits);
    }
​
    protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
    }
}

 

公平的Sync: FairSync

FairSync代表一个公平锁的实现,它的tryAcquireShared方法有自己的逻辑,与兄弟类NonfairSync的不同之处在于多了等待队列上是否存在其前驱节点的判断。

static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;
​
    FairSync(int permits) {
        super(permits);
    }
​
    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;
        }
    }
}

(2) 获取许可证

acquireUninterruptibly获取一个许可证不响应中断,若获取失败,将阻塞等待直到可获取为止

acquire获取一个许可证会响应中断,若获取失败,将阻塞等待直到可获取为止

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

 

acquire(int) 一性次获取多个许可证,响应中断。若获取失败,将阻塞等待直到可获取为止

acquireUninterruptibly(int)一性次获取多个许可证,不响应中断。,若获取失败,将阻塞等待直到可获取为止

public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}
public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}

 

tryAcquire()尝试一个获取许可证,获取失败直接返回false,不会阻塞等待。

tryAcquire(long,TimeUnit)尝试超时获取一个许可证,在限定时间内获取许可证失败返回false,不会一直阻塞等待。

    public boolean tryAcquire() {
        return sync.nonfairTryAcquireShared(1) >= 0;
    }
​
    public boolean tryAcquire(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

 

tryAcquire(int)尝试多个获取许可证,获取失败直接返回false,不会阻塞等待。

tryAcquire(int,long,TimeUnit)尝试超时获取多个许可证,在限定时间内获取许可证失败返回false,不会一直阻塞等待。

public boolean tryAcquire(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.nonfairTryAcquireShared(permits) >= 0;
}
​
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
        throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}

 

drainPermits获取并返回当前所有可用的许可证

public int drainPermits() {
    return sync.drainPermits();
}

(3) 释放许可证

release()释放一个许可证

release(int)释放多个许可证

    public void release() {
        sync.releaseShared(1);
    }
​
    public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }

(4) 减少许可证

reducePermits是protected级别方法,外部不可见,主要提供给子类在定制化某些功能时调用。

reducePermits与获取许可证方法acquireXX不同,sync.reducePermits不是锁的相关方法,不被父类AQS的模板方法调用。

reducePermits只是单单减少许可证数,不会阻塞线程。

protected void reducePermits(int reduction) {
    if (reduction < 0) throw new IllegalArgumentException();
    sync.reducePermits(reduction);
}

(5) 状态查询

availablePermits返回当前可获取的许可证数。

isFair返回当前锁的公平性。

hasQueuedThreads返回当前是否有线程因获取许可证而阻塞等待。

getQueueLength返回当前因获取许可证而阻塞等待的线程数。

getQueuedThreads返回当前因获取许可证而阻塞等待的线程集合;此方法为protected级别,外界不可见,主要方便子类监控相关指标。

    public int availablePermits() {
        return sync.getPermits();
    }
    public boolean isFair() {
        return sync instanceof FairSync;
    }
    public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
    public final int getQueueLength() {
        return sync.getQueueLength();
    }
    protected Collection<Thread> getQueuedThreads() {
        return sync.getQueuedThreads();
    }

 

 

参考: 《 Java并发编程的艺术》方腾飞

 

 

posted @ 2020-05-25 01:36  蜀中孤鹰  阅读(796)  评论(0编辑  收藏  举报