【多线程】Semaphore实现原理

news/2024/7/8 4:22:11 标签: Semaphore, 多线程

前言

Semaphore,信号量,一般用于控制同时访问资源的线程数量。可以认为Synchronized代表的是一把锁,那么Semaphore就是多把锁。


常用方法

public class Semaphore implements java.io.Serializable {
    //构造方法,传入令牌数,默认实例化一个非公平锁
    public Semaphore(int permits);
    //获取一个令牌,在获取成功之前,以及被其他线程中断之前,当前线程会被阻塞
    public void acquire() throws InterruptedException;
    //获取一个令牌,在获取成功之前,当前线程会被阻塞(中断被忽略)
    public void acquireUninterruptibly() ;
    //尝试获取令牌,立即返回获取成功与否,不阻塞当前线程
    public boolean tryAcquire();
    //释放一个令牌
    public void release();
    //返回当前可用的令牌数
    public int availablePermits();
}

现在有这样的一个例子:

某卫生间只有3个坑位,把坑前面的挡门理解为令牌,因此这里有3个令牌,现在模拟5个人抢坑位的场景。

package com.xue.testSemaphore;

import java.util.concurrent.Semaphore;

public class Main {
    public static void main(String[] args) {
        //最多支持3个人同时蹲坑
        Semaphore semaphore = new Semaphore(3);
        //5个人来抢坑位
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "已经在蹲坑");
                    //模拟蹲坑时长
                    Thread.sleep((long) (Math.random() * 10 * 1000));
                    //离开坑位
                    System.out.println(Thread.currentThread().getName() + "即将离开坑位");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, i + "号").start();
        }
    }
}

输出如下:

首先0、1、2号已经抢完了所有的坑位,3与4号只能在外面等候,对的他们没排队(默认实例化了一个非公平锁)。2号出来后,3号才能进去。接着0号出来,4号才能进去。

这个例子虽然有点俗,这确实能让人印象深刻呀。


原理解析

类图

Semaphore有2个内部类,FairSync与NonfairSync,它们都继承自Sync,Sync又继承自AQS。可以看的出来,Semaphore与CountDownLatch的结构类似,都需要借助于AQS。

对CountDownLatch不熟悉的同学,可以先参考我的另外一篇文章CountDownLatch实现原理

构造方法

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

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

默认实例化了一个非公平锁,当然也可以进行指定。这里的permits最终会传到AQS的state变量中,代表当前可用的令牌数。

acquire()

获取一个令牌,获取到线程可以继续执行,否则将会被阻塞。

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

调用了AQS中的模版方法

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取arg个令牌,该方法返回可用令牌数-需求数,如果小于0,则进行阻塞
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

其中tryAcquireShared()由具体的子类(AQS的子类Sync的子类NonfailSync)进行实现

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }

这里又调用了父类Sync的方法

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

remaining =可用令牌数-需求数<0时,直接返回remaining 。否则利用CAS进行更新,同样返回remaining 。对CAS机制不熟悉的同学,可以先参考我的另外一篇文章浅探CAS实现原理

该方法返回一个小于0的值时,将会调用以下方法,这段代码的作用主要就是将获取不到令牌的线程封装为节点,加入到阻塞队列中。

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建一个共享类型的节点加入到阻塞队列
        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);
        }
    }

release()

释放一个令牌,接着唤醒所有同步队列中的阻塞的共享模式的节点线程。被唤醒的线程重新尝试去获取令牌,获取成功则继续执行,否则重新加入到阻塞队列中。

    public void release() {
        sync.releaseShared(1);
    }

releaseShared是AQS中的模版方法

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

调用了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;
            }
        }

该方法利用CAS+循环的方式,将可用令牌数+1。更新成功后,返回ture,最后调用AQS中的doReleaseShared方法

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            if (h == head)                   
                break;
        }
    }

该方法会唤醒同步队列中所有被阻塞的共享模式的节点。

关于AQS中方法的详细解释,可能会开另外的篇幅。


http://www.niftyadmin.cn/n/1705199.html

相关文章

ListView控件演示05:获取指定坐标点的列表项

示例说明&#xff1a;代码示例演示 PictureBox 和 ListView 控件的用法。通过使用 BorderStyle 和 PictureBoxSizeMode 枚举分别设置 PictureBox.BorderStyle 和 PictureBox.SizeMode 属性来初始化 PictureBox。ListView 由 Samples 目录中的图片填充。当处理 ListView 控件的 …

负载均衡技术沙龙问答汇集(第一期)

抽选了一些博友的一些比较典型的问题作答,欢迎参与讨论1、问&#xff1a;lvs方案是否适用于windows环境&#xff1f;答&#xff1a;可以。目前lvs转发器只支持linux等&#xff0c;但真是服务器就可以是各种操作系统及应用。问题的关键在于设置windows的虚拟ip的掩码为4个255。2…

JavaSE 集合类HashSet保证自定义对象唯一性

首先我们自定义Person类&#xff0c;只有姓名和年龄两个属性 class Person{private String name ;private int age ;public Person(String name, int age) {super();this.name name;this.age age;}public String getName() {return name;}public void setName(String name) …

【多线程】说说线程池

前言 线程池内部是多个线程的集合&#xff0c;在创建初期&#xff0c;线程池会创建出多个空闲的线程&#xff0c;当有一个任务需要执行时&#xff0c;线程池会选择出一个线程去执行它&#xff0c;执行结束后&#xff0c;该线程不会被销毁&#xff0c;而是可以继续复用。 使用…

像咨询师一样思考---走出软件作坊:三五个人十来条枪 如何成为开发正规军(三十一)...

我一直在想办法提升管理软件的销售价。在软件产品方面&#xff0c;分了高级版、标准版、简化版。并且引入了测试&#xff0c;提高产品质量。引入了文案&#xff0c;制作了产品白皮书、操作帮助说明、安装说明、配置说明、维护说明、新版本更新说明、操作视频、演示版。在软件UI…

ListView控件演示06:如何实现用户同时选择多个列表项

代码示例演示一个允许选择多项的ListView。其实也就是把ListView.MultiSelect属性值设置为true&#xff0c;这样便能够同时选择多个列表项了。相反&#xff0c;则一次只能选择一个。默认属性值为true。 该示例演示如何设置HideSelection和HeaderStyle属性。它还演示了ColumnHea…

【多线程】LongAdder实现原理

前言 AtomicInteger、AtomicLong使用非阻塞的CAS算法原子性地更新某一个变量&#xff0c;比synchronized这些阻塞算法拥有更好的性能&#xff0c;但是在高并发情况下&#xff0c;大量线程同时去更新一个变量&#xff0c;由于同一时间只有一个线程能够成功&#xff0c;绝大部分…

2008年招录老婆统一考试试卷

2008年招录老婆统一考试试卷 ①本试卷为男士招录老婆统一考试试卷&#xff0c;各地均须使用此卷&#xff0c;不得自行命题。 ②由于法规未规定同性婚姻合法&#xff0c;故报名参加考试者均须为女性。若男性报名&#xff0c;须于开考前到指定医院做变性手术&#xff0c;否则取消…