线程之Semaphore基本内容

 

1. 信号量Semaphore

Semaphore的实现借助同步框架AQS,Semaphore使用一个内部类Sync来实现,而Sync继承AQS来实现,Sync有两个子类,分别对应公平模式和非公平模式的Semaphore 。
Semaphore代码结构:首先Sync构造函数:Sync(int permits) { setState(permits); }
参数即为需要管理的许可数量,Sync使用AQS提供的setState方法来初始化共享变量state,后续通过acquire和release来获取和规划许可。
下图片展示Semaphore代码结构,

一. Semaphore的基本概念
1. Semaphore介绍:
目的:调度线程:一些线程生产(increase),同时另一些线程消费(decrease)。 Semaphore可让生产和消费保持合乎逻辑的执行顺序。
有的人用 Semaphore也可以把上面例子中的票“保护"起来,以防止共享资源冲突,必须承认这是可行的,但是Semaphore 不是让你用来做这个的。如果你要做这件事,请用Mutex。

Semephore类是java.util.concurrent包下处理并发的工具类,Semephore能够控制任务访问资源的数量,如果资源不够,则任务阻塞,等待其他资源的释放。
Semaphore只是使用AQS的一种简单例子,AQS强大之处在于,你仅仅需要继承他,然后使用它提供api就可以实现任意复杂的线程同步方案。
AQS为我们做了大部分同步工作,所以本文是对使用AQS的一种简单介绍,你当去分析一下AQS的实现细节,并且加以总结。
这么说,理解AQS就理解java中线程同步是如何实现的。线程同步是并发的核心内容,如何保证多个线程可以安全高效的访问共享数据,是并发需要首要考虑的问题,而AQS解决了这些问题,未来还会对AQS进行深入分析总结。

引入:
Semaphore实现为一种基于计数的信号量,Semaphore管理着一组虚拟的许可集合,这种许可可以作为某种凭证,来管理资源,在一些资源有限的场景下很有实用性。
比如数据库连接,应用可初始化一组数据库连接,然后通过使用Semaphore来管理获取连接的许可,任何线程想要获得一个连接必须首先获得一个许可,然后再凭这个许可获得一个连接,这个许可将持续到这个线程归还了连接。
在使用上,任何一个线程都需要通过acquire来获得一个Semaphore许可,这个操作可能会阻塞线程直到成功获得一个许可,因为资源是有限的,所以许可也是有限的,没有获得资源就需要阻塞等待其他线程归还Semaphore,而归还Semaphore操作通过release方法来进行,release会唤醒一个等待在Semaphore上的一个线程来尝试获得许可。

如果想要达到一种互斥的效果,比如任何时刻只能有一个线程获得许可,那么可以初始化Semaphore的数量为1,一个线程获得这个Semaphore之后,任何到来的通过acquire来尝试获得许可的线程都会被阻塞直到这个持有Semaphore的线程调用了release方法来释放Semaphore。
在实现上,Semaphore借助线程同步框架AQS,AQS的分析可以参考文章Java同步框架AbstractQueuedSynchronizer,
同样借助了AQS来实现的是java中的可重入锁的实现,同样可在文章Java可重入锁详解中找到java中可重入锁的分析总结文档。
在这些文章中已经分析过如何通过AQS来实现锁的语义。

Semaphore作为一种线程间同步机制是非常轻量级的方案,所以学习Semaphore是有必要的。
Semaphore(信号量) 是一个线程同步结构,用于在线程间传递信号,以避免出现信号丢失,或者像锁一样用于保护一个关键区域.
Semaphore可以用于做流量控制,特别公用资源有限的应用场景,比如:数据库连接。
Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。
Semaphore可以控制某个资源可被同时访问的个数,
通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
其中在执行semaphore.acquire()方法时,阻塞时间不同,因此前五个数据在打印时顺序不同,但之后每次都只会释放一个接受一个,故此,之后都是按顺序来排列的。

一个最典型的使用Semaphore 的场景:
a源自一个线程,b源自另一个线程,计算 c = a + b 也是一个线程。显然,第三个线程必须等第一、二个线程执行完毕它才能执行。
在这个时候,我们就需要调度线程了:让第一、二个线程执行完毕后,再执行第三个线程。
比如一个停车场有三个车位,当车位空余数量小于3时,车辆可以进入停车场停车。如果停车场已经没有了空余车位,后面来的车就不能进入停车场,
只能在停车场外等待,等其他车辆离开之后才能进入。

Semaphore 注意事项:
1).它的原理就是AQS。 Semaphore只是使用AQS的一种简单例子。
  Semaphore借助了线程同步框架AQS。 AQS分析可参考:Java同步框架AbstractQueuedSynchronizer。
2). Semaphore(int permits, boolean fair)公平与非公平:
当一个线程 release 释放了一个许可后,fair 决定了正在等待的线程该由谁获取许可。
如果是公平竞争则等待时间最长的线程(基本上是最早建的线程)获取,如果是非公平竞争则随机选择一个线程获取许可。不传 fair 的构造函数默认采用非公开竞争。 fair这个参数则表示是否是公平的,即等待时间越久的线程越能优先获得许可访问的权限。
3).需要注意是,任何线程在获得许可之后,使用共享资源完毕都需要执行归还操作,否则会有线程一直在等待。
// 阻塞函数,直到有可访问的线程,才继续运行
semaphore.acquire();

2.1 使用流程:
Semaphore实现为一种基于计数的信号量,Semaphore管理着一组虚拟的许可集合,这种许可可以作为某种凭证,来管理资源,在一些资源有限的场景下很有实用性,比如数据库连接,应用可初始化一组数据库连接,然后通过使用Semaphore来管理获取连接的许可,任何线程想要获得一个连接必须首先获得一个许可,然后再凭这个许可获得一个连接,这个许可将持续到这个线程归还了连接。
在使用上,任何一个线程都需要通过acquire来获得一个Semaphore许可,这个操作可能会阻塞线程直到成功获得一个许可,因为资源是有限的,所以许可也是有限的,没有获得资源就需要阻塞等待其他线程归还Semaphore,而归还Semaphore操作通过release方法来进行,release会唤醒一个等待在Semaphore上的一个线程来尝试获得许可。

2.2 实现互斥锁功能:
如任何时刻只能有一个线程获得许可,那么设置Semaphore的数量为1,一个线程获得这个Semaphore之后,任何到来的通过acquire来尝试获得许可的线程都会被阻塞,直到这个持有Semaphore的线程调用了release方法来释放Semaphore。
Semaphore s = new Semaphore(1)",也就是该信号量的初始permits是1,但是在此后每次调用release方法都会导致permits加一。 如果能限制permits最大值1,最小值0,那就是真正的Mutex了。

2.3 信号量用于线程同步,互斥量用户保护资源的互斥访问。
信号量与互斥量的区别:
互斥量用于线程的互斥,信号线用于线程的同步。
互斥量值只能为0/1,信号量值可以为非负整数。信号量可以实现多个同类资源的多线程互斥和同步。
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

2.4 信号量解决生产者-消费者问题?

3. Semephore类的主要方法:
公平锁和非公平锁:程序在执行并发任务的时候,拿到同步锁的任务执行代码,其他任务阻塞等待,一旦同步锁被释放,CPU会正在等待的任务分配资源,获取同步锁。
在这里又两种策略,CPU默认从等待的任务中随机分配,这是非公平锁;
公平锁是按照等待时间优先级来分配,等待的时间越久,先获取任务锁。其内部是一个同步列队实现的。

3.1、默认构造器
public Semaphore(int permits) {
  sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
fair这个参数则表示是否是公平的,即等待时间越久的线程越能优先获得许可访问的权限。

3.2、重要方法
public void acquire() throws InterruptedException {
   sync.acquireSharedInterruptibly(1);
}
public void acquire(int permits) throws InterruptedException {
   if (permits < 0) throw new IllegalArgumentException();
   sync.acquireSharedInterruptibly(permits);
}
public void release() {
  sync.releaseShared(1);
}
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
acquire()用来获取一个许可,而acquire(int permits)则是用来获取permits个许可,如果暂时无许可可获取,则会一直等待,直到获得许可;
release()表示释放许可,而release(int permits) 则是表示释放permits个许可,
另外在调用这两个方法的时候需要注意,在释放许可之前,必须要先获得许可才行。
除此之外,上面4个方法在被调用后都会出现阻塞,而如果我们想立即得到执行结果,则可以直接使用下面几个方法:
尝试获取一个许可,若获取成功,则立即返回true;若获取失败,则立即返回false。

// 尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire() {
  return sync.nonfairTryAcquireShared(1) >= 0;}

// 尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
  return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(int permits) {
  if (permits < 0) throw new IllegalArgumentException();
  return sync.nonfairTryAcquireShared(permits) >= 0;
}
//尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit){
  if (permits < 0) throw new IllegalArgumentException();
  return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}

3.3、其他方法:
返回此信号量中当前可用的许可证数
public int availablePermits() {
   return sync.getPermits();
}
返回正在等待获取许可证的线程数
public final int getQueueLength() {
  return sync.getQueueLength();
}
是否有线程正在等待获取许可证
public final boolean hasQueuedThreads() {
  return sync.hasQueuedThreads();
}
减少reduction个许可证。是个protected方法
protected void reducePermits(int reduction) {
  if (reduction < 0) throw new IllegalArgumentException();
  sync.reducePermits(reduction);
}
返回所有等待获取许可证的线程集合。是个protected方法
protected Collection<Thread> getQueuedThreads() {
  return sync.getQueuedThreads();

二. 线程之Semaphore基本内容

更多Java 并发编程方面的文章,请参见文集《Java 并发编程》

Lock
Lock是一个抽象概念。使得只有一个线程可以访问某个资源,并且Lock是不能被其他线程共享的。
Mutex: 全程 MUTual EXclusion。
目的:保护共享资源。
典型实例就是买票:票是共享资源,现在有两个线程同时过来买票。如果你不用 Mutex在线程里把票锁住,那么就可能出现“把同一张票卖给两个不同的人(线程)”的情况。

Semaphore 的使用

所在包:java.util.concurrent:

信号量 Semaphore 可以控制同时访问某个资源的线程个数。

  • public Semaphore(int permits) 构造方法,设置许可的个数,默认为非公平锁
  • public Semaphore(int permits, boolean fair) 构造方法,设置许可的个数,可以设置为公平锁(即等待越久的线程优先获得 permits)或非公平锁
  • void acquire() 获得一个许可 permits,没有的话,线程就阻塞
  • void acquire(int arg) 获得多个许可 permits,没有的话,线程就阻塞
  • boolean tryAcquire() 获得一个许可 permits,没有的话就返回 false,线程不阻塞
  • boolean tryAcquire(int permits) 获得多个许可 permits,没有的话就返回 false,线程不阻塞
  • void release() 释放一个许可 permits,在释放之前,必须先获得许可 permits
  • void release(int permits) 释放多个许可 permits,在释放之前,必须先获得许可 permits
  • int availablePermits() 得到当前可用的许可数目

acquire():获取许可,Semephore任务数加一
release():释放许可,Semephore任务数减一
tryAcquire()尝试获取许可,返回boolean值,不阻塞。
availablePermits()还剩几个任务许可,等等几个方法和Lock类的用法相似。

Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

Semaphore

Semaphore 有两个构造函数,参数为许可的个数 permits 和是否公平竞争 fair。通过 acquire 方法能够获得的许可个数为 permits,如果超过了这个个数,就需要等待。当一个线程 release 释放了一个许可后,fair 决定了正在等待的线程该由谁获取许可,

如果是公平竞争则等待时间最长的线程获取,如果是非公平竞争则随机选择一个线程获取许可。不传 fair 的构造函数默认采用非公开竞争。

Semaphore(int permits)
Semaphore(int permits, boolean fair)

一个线程可以一次获取一个许可,也可以一次获取多个。 在 acquire 等待的过程中,如果线程被中断,acquire 会抛出中断异常,

如果希望忽略中断继续等待可以调用 acquireUninterruptibly 方法。同时提供了 tryAcquire 方法尝试获取,获取失败返回 false,获取成功返回 true。

tryAcquire 方法可以在获取不到时立即返回,也可以等待一段时间。

需要注意的是,没有参数的 tryAcquire 方法在有许可可以获取的情况下,无论有没有线程在等待都能立即获取许可,即便是公平竞争也能立即获取。

public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()
public void acquire(int permits)
public void acquireUninterruptibly(int permits)
public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
public void release(int permits) 

使用实例:

测试方法 test 创建了多个线程,每个线程启动后都调用 acquire 方法,然后延时 5s 模仿业务耗时,最后调用 release 方法释放许可。

public class SemaphoreTest {
    private int threadNum;
    private Semaphore semaphore;
    public SemaphoreTest(int permits,int threadNum, boolean fair) {
        this.threadNum = threadNum;
        semaphore = new Semaphore(permits,fair);
    }
    private void println(String msg){
        SimpleDateFormat sdf = new SimpleDateFormat("[YYYY-MM-dd HH:mm:ss.SSS] ");
        System.out.println(sdf.format(new Date()) + msg);
    }
    public void test(){
        for(int i =  0; i < threadNum; i ++){
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    println(Thread.currentThread().getName() + " acquire");
                    Thread.sleep(5000);//模拟业务耗时
                    println(Thread.currentThread().getName() + " release");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

公平与非公平

在上述的示例中,如果 fair 传的是 true,则各个线程公平竞争,即按照等待时间的长短决定谁先获取许可。

以 9 个线程竞争 3 个许可为例,执行结果如下,首选是线程 0、1、2 获取了许可,5s 后线程 3、4、5 获取了许可,最后是线程 6、7、8 获取许可,

顺序基本上与创建线程并启动的先后顺序一致,也与各个线程等待的时间基本相符。

[2017-08-20 21:47:21.817] Thread-0 acquire
[2017-08-20 21:47:21.817] Thread-2 acquire
[2017-08-20 21:47:21.817] Thread-1 acquire
[2017-08-20 21:47:26.830] Thread-1 release
[2017-08-20 21:47:26.830] Thread-0 release
[2017-08-20 21:47:26.830] Thread-4 acquire
[2017-08-20 21:47:26.830] Thread-3 acquire
[2017-08-20 21:47:26.831] Thread-2 release
[2017-08-20 21:47:26.831] Thread-5 acquire
[2017-08-20 21:47:31.831] Thread-4 release
[2017-08-20 21:47:31.831] Thread-3 release
[2017-08-20 21:47:31.831] Thread-6 acquire
[2017-08-20 21:47:31.831] Thread-7 acquire
[2017-08-20 21:47:31.832] Thread-5 release
[2017-08-20 21:47:31.832] Thread-8 acquire
[2017-08-20 21:47:36.831] Thread-6 release
[2017-08-20 21:47:36.831] Thread-7 release
[2017-08-20 21:47:36.832] Thread-8 release

在上述的示例中,如果 fair 传的是 false,则各个线程非公平竞争,随机选取一个线程获取许可。

以 9 个线程竞争 3 个许可为例,执行结果如下,首先是线程 0、1、3 获取了许可,5s 后线程 2、5、7 获取了许可,最后是线程 4、6、8 获取许可,

与线程创建启动时间无关,也与线程等待时间无关。

[2017-08-20 17:45:09.893] Thread-0 acquire
[2017-08-20 17:45:09.893] Thread-3 acquire
[2017-08-20 17:45:09.893] Thread-1 acquire
[2017-08-20 17:45:14.895] Thread-3 release
[2017-08-20 17:45:14.895] Thread-0 release
[2017-08-20 17:45:14.895] Thread-5 acquire
[2017-08-20 17:45:14.895] Thread-1 release
[2017-08-20 17:45:14.896] Thread-7 acquire
[2017-08-20 17:45:14.896] Thread-2 acquire
[2017-08-20 17:45:19.895] Thread-5 release
[2017-08-20 17:45:19.895] Thread-4 acquire
[2017-08-20 17:45:19.896] Thread-7 release
[2017-08-20 17:45:19.896] Thread-6 acquire
[2017-08-20 17:45:19.896] Thread-2 release
[2017-08-20 17:45:19.896] Thread-8 acquire
[2017-08-20 17:45:24.895] Thread-4 release
[2017-08-20 17:45:24.896] Thread-8 release
[2017-08-20 17:45:24.896] Thread-6 release
posted on 2020-04-01 23:37  左手指月  阅读(489)  评论(0编辑  收藏  举报