【细谈Java并发】谈谈Semaphore

1、简介

在java中,使用了synchronized关键字和Lock锁实现了资源的并发访问控制,在同一时间只允许唯一了线程进入临界区访问资源(读锁除外),这样子控制的主要目的是为了解决多个线程并发同一资源造成的数据不一致的问题。在另外一种场景下,一个资源有多个副本可供同时使用,比如打印机房有多个打印机、厕所有多个坑可供同时使用,这种情况下,Java提供了另外的并发访问控制--资源的多副本的并发访问控制,今天学习的信号量Semaphore即是其中的一种。

2、使用场景

假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

public class SemaphoreTest {

    // 初始化5个资源(机器)
    private final static Semaphore SEMAPHORE = new Semaphore(5);

    public static void main(String[] args) {
        // 8个工人争夺资源
        for (int i = 0; i < 8; i++) {
            final String name = "工人" + i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        SEMAPHORE.acquire();
                        System.out.println(name + "占用一个机器在生产...");
                        Thread.sleep(2000);
                        System.out.println(name + "释放出机器");
                        SEMAPHORE.release();
                    } catch (InterruptedException e) {
                        System.out.println(name + "争夺机器失败");
                    }
                }
            }).start();
        }
    }
}

输出结果

工人0占用一个机器在生产...
工人4占用一个机器在生产...
工人3占用一个机器在生产...
工人2占用一个机器在生产...
工人1占用一个机器在生产...
工人4释放出机器
工人2释放出机器
工人0释放出机器
工人6占用一个机器在生产...
工人3释放出机器
工人1释放出机器
工人7占用一个机器在生产...
工人5占用一个机器在生产...
工人7释放出机器
工人6释放出机器
工人5释放出机器

3、原理分析

我们会先从构造函数开始到AQS的子类Sync开始分析,接着分析NonfairSync和FairSync,以及类里的重要方法,一步步的揭开它神秘的面纱。

建议阅读本文之前先熟悉AQS的原理,关于AQS可以移步:【细谈Java并发】谈谈AQS

3.1、构造函数

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

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

这个构造函数和ReentrantLock很相似,默认都是非公平锁,唯一的区别就是Semaphore需要提供一个默认permits。

3.2、Sync

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;

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

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

    // release会调用
    protected final boolean tryReleaseShared(int releases) {
        for (;;) {  // 自旋CAS
            int current = getState();
            // 释放资源,增加state值,并进行CAS替换
            int next = current + releases;
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next))
                return true;
        }
    }

    // 减少许可值
    final void reducePermits(int reductions) {
        for (;;) {  // 自旋CAS
            int current = getState();
            // 减少资源的state值,并进行CAS替换
            int next = current - reductions;
            if (next > current) // underflow
                throw new Error("Permit count underflow");
            if (compareAndSetState(current, next))
                return;
        }
    }

    // 清空许可值
    final int drainPermits() {
        for (;;) {
            int current = getState();
            if (current == 0 || compareAndSetState(current, 0))
                return current;
        }
    }
}

Sync里的方法都很简单,都是通过自旋CAS来设置许可值。

NonfairSync和FairSync

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -2694183684443567898L;

    NonfairSync(int permits) {
        super(permits);
    }

    /**
     * 可用资源 < 0 直接返回
     * 可用资源 >= 0 CAS替换
     */
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                return remaining;
        }
    }
}

static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;

    FairSync(int permits) {
        super(permits);
    }

    /**
     * Sync队列前有
     * 可用资源 < 0 直接返回
     * 可用资源 >= 0 CAS替换
     */
    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;
        }
    }
}

实现公平性的主要就是hasQueuedPredecessors() 这个方法,它用来返回队列中是否有比当前线程等待更久的线程。这也是和NonfairSync的唯一区别的地方。

3.3、acquire相关方法

// 共享阻塞获取资源,interrupt后抛出异常
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// 共享获取资源,interrupt后不会抛出异常
public void acquireUninterruptibly() {
    sync.acquireShared(1);
}

// 共享阻塞获取资源,interrupt后抛出异常,可以指定获取的资源值
public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}

// 共享获取资源,interrupt后不会抛出异常,可以指定获取的资源值
public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}

// 尝试获取资源,如果能够获取到返回true,否则返回false
public boolean tryAcquire() {
    return sync.nonfairTryAcquireShared(1) >= 0;
}

// 在timeout里尝试获取资源,如果能够获取到返回true,否则返回false
public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

// 尝试获取指定资源,如果能够获取到返回true,否则返回false
public boolean tryAcquire(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.nonfairTryAcquireShared(permits) >= 0;
}

// 在timeout里尝试获取指定资源,如果能够获取到返回true,否则返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
        throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}

3.4、release相关方法

// 共享释放资源
public void release() {
    sync.releaseShared(1);
}

// 共享释放指定资源
public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}

3.5、其他方法

// 获取可用资源
public int availablePermits() {
    return sync.getPermits();
}

// 清空资源值
public int drainPermits() {
    return sync.drainPermits();
}

// 减少指定资源值
protected void reducePermits(int reduction) {
    if (reduction < 0) throw new IllegalArgumentException();
    sync.reducePermits(reduction);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 1、概述 谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈Abstract...
    蹲厕所的熊阅读 1,811评论 4 36
  • 未来都是要等待。 或许是像放学的孩子等待父母,又或是熟记讲稿等待隔日的发言。 但我们都预料不了结果,也不知道每个人...
    周淡皮阅读 126评论 0 0
  • 姓名:丁秋红 所在公司:宁波绿之品电器科技有限公司 组别: 340期【反省一组】学员 一【知~学习】 1、朗诵《六...
    丁小博阅读 229评论 0 0
  • 一. 下载ubuntu16.04 直接到官网免费下载http://www.ubuntu.com/download/...
    拍个黄瓜G阅读 270评论 0 0
  • 严歌苓的《床畔》我已经读完很久了,依旧是拿到了书,很快就看完的。她的文字就是这么有魅力,让人拿起书来就欲罢不能。 ...
    Cola猫咪阅读 176评论 0 1