【多线程并发编程】十四 面试官:说说Java中的信号量?Semaphore

程序猿学社的GitHub,欢迎Star
https://github.com/ITfqyd/cxyxs
本文已记录到github,形成对应专题。

前言

随着18年开始,各个公司的面试难度,也不断在提升,面试也不仅仅停留再ArrayList底层是如何实现的。本文就来介绍一下,我们面试经常会问的一个Semaphore,CountDownLatch、CyclicBarrier都有什么区别。本来 就来说道说道Semaphore。

概念

Semaphore是jdk 1.5以引入的,同版本引入的还有他的两个同胞兄弟,CountDownLatch和CyclicBarrier。位于java.util.concurrent报下,一般简称juc。
一个计数信号量。 在概念上,信号量维持一组许可证,如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证。

应用场景

  • 晚上我们去愿者上钩吃烤鱼,可以看到路外面站着一排排的人,都在做着等叫号,实际上也就是获取许可。
  • 茅坑的故事,茅坑是固定的,假设为5个,同一时刻来了10个人,还有5个人就得等待。
  • 公司园内停车,实际上车位也是固定的,听不下去,保安就会让你停到其他地方(当然,社长,买不起车,所以,就没有这种烦恼)

api接口

方法 描述
Semaphore(int permits) 创建一个 Semaphore与给定数量的许可证和非公平公平设置
Semaphore(int permits, boolean fair) 创建一个 Semaphore与给定数量的许可证和给定的公平设置。
acquire() 从该信号量获取许可证,阻止直到可用
acquire(int permits) 从该信号量获取给定数量的许可证,阻止直到所有可用
release() 释放许可证
release(int permits) 释放一定数量的许可证
  • 列举一些常用的api接口

实战

模拟一个餐厅叫号,吃饭,结算的一个场景,有4张单人桌子,来了10个客人过来吃饭的一个场景。通过信号量来实现。

package com.cxyxs.thread.fourteen;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/6 18:41
 * Modified By:
 */
public class Demo1 {
    public static void main(String[] args) {
        //4表示4张桌子
        Semaphore semaphore = new Semaphore(4);
        for (int i=0;i<10;i++) {
            final int  index=i;
            new Thread(()->{
                try {
                    //1.被服务员叫号  分发许可证
                    semaphore.acquire();
                    //2.开始吃饭
                    System.out.println(Thread.currentThread().getName()+"被服务员叫号后,准备上桌吃饭");
                    //3.吃饭中  模拟吃饭2s
                    TimeUnit.SECONDS.sleep(2);
                    //4.吃完饭,喊服务员结算
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName()+"喊服务员结算!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"客人"+index).start();
        }
    }
}

运行结果

客人0被服务员叫号后,准备上桌吃饭
客人2被服务员叫号后,准备上桌吃饭
客人1被服务员叫号后,准备上桌吃饭
客人3被服务员叫号后,准备上桌吃饭
客人1喊服务员结算!
客人6被服务员叫号后,准备上桌吃饭
客人3喊服务员结算!
客人5被服务员叫号后,准备上桌吃饭
客人4被服务员叫号后,准备上桌吃饭
客人7被服务员叫号后,准备上桌吃饭
客人2喊服务员结算!
客人0喊服务员结算!
客人4喊服务员结算!
客人5喊服务员结算!
客人9被服务员叫号后,准备上桌吃饭
客人8被服务员叫号后,准备上桌吃饭
客人6喊服务员结算!
客人7喊服务员结算!
客人9喊服务员结算!
客人8喊服务员结算!
  • 通过上面的结果,我们可以发现,因为只有4张桌子,每次只能容纳4个人,所以每次同时都有4个人得到许可。
  • 客人1结算后,客人6就被叫号咯。
  • new Semaphore(4)我们应该理解了把,表示同一时刻,只能有多少个线程同时运行。
  • Semaphore内部也是基于AQS实现的,也就是AbstractQueuedSynchronizer的简写。
  • 默认采用非公平锁。
  • 在实际的业务场景中,限流可能会用到。

公平锁

   Semaphore semaphore = new Semaphore(4,true);
  • 第二个参数表示,默认为false,表示不公平锁,true为公平锁。
    公平锁,既先进先出,可以理解为消息队列,先生存的消息,在队列的头部。
    不公平锁,既可能存在插队现象。

浅谈源码

Semaphore重构方法

   public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }
  • 发现有FairSync和NonfairSync两个类,既公平锁和非公平锁。如果为true,则为公平锁。
 private volatile int state;
  /**
     * Sets the value of synchronization state.
     * This operation has memory semantics of a {@code volatile} write.
     * @param newState the new state value
     */
    protected final void setState(int newState) {
        state = newState;
    }
  • 实际上,setState这个方法,不要被这个方法名,混淆了我们的视觉,实际上,这个方法就是调用父类AQS的setState,设置计数。

acquire方法获取许可

public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }
  • 不能这是为负数,负数会抛出IllegalArgumentException异常
   public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
  • 减去获取的许可,判断会不会小于0
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;
            }
        }
  • for (; ) 是不是看起来也很少见,等同于while(true),实际上在源码里面几乎都是使用的这种方式。指令少,不占用寄存器,而且没有判断跳转
  • 判断原有的许可的计数减去需要获取的是否为空。
  • 这又是我们的老朋友CAS(compareAndSetState的简称)

打野CAS

简单的理解CAS是怎么一回事。并且,他经常露脸压线,不抓你,抓谁。通过一个简单的小例子,理解一下CAS

  public static void main(String[] args) {
       AtomicInteger atomicInteger = new AtomicInteger(1);
        atomicInteger.compareAndSet(1,2);
        System.out.println(atomicInteger.get());
    }

  • 他的结果是2,why?
  • 实际上他的初始化为1,调用compareAndSet方法,会判断跟原来的值是否相等,如果相等,则改为2。是不是还有有点小懵逼。别急。我们来看看他的源码。
 public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
  • expect 期望的值,update需要修改的值。
  • 如果当前的值和我期望的值相等,则改为update参数对应的值。

release释放许可

 public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }
  • 调用父类AQS的releaseShared方法
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;
            }
        }
  • 跟获取信号量的源码类似,只是一个是减去releases,释放则为增加releases操作。

源码学习实际上没有我们想象中的类,最近我们看了几个类,发现底层的调用都类似。
觉得不错,可以通过github关注我的工号,文章已整理成对应的专题,会不定期的推送。
github

发布了289 篇原创文章 · 获赞 1080 · 访问量 36万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览