多线程高并发编程(6) -- Semaphere、Exchanger源码分析
一.Semaphere
1.概念
一个计数信号量。在概念上,信号量维持一组许可证。如果有必要,每个acquire()
都会阻塞,直到许可证可用,然后才能使用它。每个release()
添加许可证,潜在地释放阻塞获取方。但是,没有使用实际的许可证对象;Semaphore
只保留可用数量的计数,并相应地执行。即一个Semaphore维护了一组permits【许可证】。每次调用acquire()方法都会阻塞,直到获取到许可证。每次调用release()方法都会添加一个许可证,也就是释放一个被阻塞的获取者。但是实际上并不存在这个许可证,Semaphore仅仅是记录可用资源的数量,并且做出对应的行为(有资源就获取,没有资源就阻塞)。
信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
线程池控制的是线程数量,而信号量控制的是并发数量,虽然说看起来一样,但两者还是有区别的。
信号量类似于锁机制,信号量的调用,当达到数量后,线程还是存在的,只是被挂起了而已。而线程池,同时执行的线程数量是固定的,超过了数量的只能等待。
在获得项目之前,每个线程必须从信号量获取许可证,以确保某个项目可用。当线程完成该项目后,它将返回到池中,并将许可证返回到信号量,允许另一个线程获取该项目。请注意,当调用acquire()时,不会保持同步锁定,因为这将阻止某个项目返回到池中。信号量封装了限制对池的访问所需的同步,与保持池本身一致性所需的任何同步分开。【即将限制对池的访问和对池中数据的操作所需要的锁分开】。
信号量被初始化为一个,并且被使用,使得它只有至多一个允许可用,可以用作互斥锁。这通常被称为二进制信号量,因为它只有两个状态:一个许可证可用,或零个许可证可用。当以这种方式使用时,二进制信号量具有属性(与许多Lock实现不同),“锁”可以由除所有者之外的线程释放(因为信号量没有所有权概念)。这在某些专门的上下文中是有用的,例如死锁恢复。
Semaphore(int permits) 创建一个 Semaphore与给定数量的许可证和非公平公平设置。
Semaphore(int permits, boolean fair) 创建一个 Semaphore与给定数量的许可证和给定的公平设置。
此类的构造函数可选择接受公平参数。当设置为false时,此类不会保证线程获取许可的顺序。特别是,闯入是允许的,也就是说,一个线程调用acquire()可以提前已经等待线程分配的许可证-在等待线程队列的头部逻辑新的线程将自己【新线程将自己放在等待线程队列的最前面】。当公平设置为真时,信号量保证调用acquire方法的线程被选择以按照它们调用这些方法的顺序获得许可(先进先出; FIFO)【FIFO的顺序是指是依据到达方法内部的执行点的时间,并不是方法执行的时间。】。请注意,FIFO排序必须适用于这些方法中的特定内部执行点。因此,一个线程可以在另一个线程之前调用acquire,但是在另一个线程之后到达排序点,并且类似地从方法返回。另请注意,未定义的tryAcquire方法不符合公平性设置,但将采取任何可用的许可证。【不定时的tryAcquire()方法会任意选取可用的许可证。】【非公平锁可以插队获取运行,公平锁按照线程顺序执行。】
通常,用于控制资源访问的信号量应该被公平地初始化,以确保线程没有被访问资源【确保没有线程因为长时间获取不到许可证而饿死】。当使用信号量进行其他类型的同步控制时,非正常排序的吞吐量优势往往超过公平性。
2.用法
线程可以通过acquire()方法获取到一个许可,然后对共享资源进行操作,如果许可集已分配完了,那么线程将进入等待状态,直到其他线程释放许可才有机会再获取许可,线程释放一个许可通过release()方法完成,"许可"将被归还给Semaphore。
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
final Semaphore sp = new Semaphore();
for (int i = ; i < ; i++) {
Runnable runnable = () -> {
try {
sp.acquire();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() +
"进入,当前已有" + ( - sp.availablePermits()) + "个并发");
try {
Thread.sleep((long) (Math.random() * ));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() +
"即将离开");
sp.release();
//下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
System.out.println("线程" + Thread.currentThread().getName() +
"已离开,当前已有" + ( - sp.availablePermits()) + "个并发");
};
service.execute(runnable);
}
}
结果:
线程pool--thread-1进入,当前已有1个并发
线程pool--thread-2进入,当前已有2个并发
线程pool--thread-3进入,当前已有3个并发
线程pool--thread-3即将离开
线程pool--thread-4进入,当前已有3个并发
线程pool--thread-3已离开,当前已有3个并发
线程pool--thread-1即将离开
线程pool--thread-1已离开,当前已有2个并发
线程pool--thread-5进入,当前已有3个并发
线程pool--thread-5即将离开
线程pool--thread-5已离开,当前已有2个并发
线程pool--thread-6进入,当前已有3个并发
线程pool--thread-4即将离开
线程pool--thread-4已离开,当前已有2个并发
线程pool--thread-7进入,当前已有3个并发
线程pool--thread-2即将离开
线程pool--thread-2已离开,当前已有2个并发
线程pool--thread-7即将离开
线程pool--thread-7已离开,当前已有1个并发
线程pool--thread-6即将离开
线程pool--thread-6已离开,当前已有0个并发
3.acquire解析
acquire() 从该信号量获取许可证,阻止直到可用,或线程为 interrupted 。
void acquire(int permits) 从该信号量获取给定数量的许可证,阻止直到所有可用,否则线程为 interrupted 。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);//调用AQS的acquireSharedInterruptibly
}
/**
* AQS的acquireSharedInterruptibly
* Acquires in shared mode, aborting if interrupted. Implemented
* by first checking interrupt status, then invoking at least once
* {@link #tryAcquireShared}, returning on success. Otherwise the
* thread is queued, possibly repeatedly blocking and unblocking,
* invoking {@link #tryAcquireShared} until success or the thread
* is interrupted.
* 以共享模式获取,如果中断被中止。
* 实现首先检查中断状态,然后至少调用一次tryacquirered,成功返回。
* 否则,线程排队,可能会重复阻塞和取消阻塞,
* 调用tryacquiremred直到成功或线程被打断了。
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())//中断抛出异常
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)//获取失败,加入同步队列等待
doAcquireSharedInterruptibly(arg);
}
//由Semaphore的FairSync或NonfairSync实现,共享模式下资源可以被多个线程通知占用,tryAcquireShared返回int类型,表示还有多少个资源可以同时被占用,用于共享模式下传播唤醒。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//以共享中断模式获取
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);//创建当前线程的节点,并且锁的模型是共享锁,将其添加到AQS CLH队列的末尾
boolean failed = true;
try {
for (;;) {//自旋
final Node p = node.predecessor();//获得当前节点的前驱节点
if (p == head) {//是头节点,没有等待节点
int r = tryAcquireShared(arg);
if (r >= 0) {//获取成功当前节点设置为头节点并传播【传播指的是,同步状态剩余的许可数值不为0,通知后续结点继续获取同步状态】
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//前继节点非head节点,没资源获取,将前继节点状态设置为SIGNAL,通过park挂起node节点的线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);//结束该结点线程的请求
}
}
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();//数量小于0抛出异常
sync.acquireSharedInterruptibly(permits);//调用AQS的acquireSharedInterruptibly
}
tryAcquireShared:
static final class FairSync extends Sync {//公平锁获取
protected int tryAcquireShared(int acquires) {
for (;;) {//自旋
//有前驱节点,表示当前线程前面有阻塞线程,当前线程获取失败,先让前节点线程获取运行【比非公平锁获取多了判断前驱节点的操作】
if (hasQueuedPredecessors())
return -1;
int available = getState();//可获取的许可证数量
int remaining = available - acquires;//剩下的许可证数量
//如果剩余数量小于0或更新剩余数量成功,返回剩余数量
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
static final class NonfairSync extends Sync {//非公平锁获取
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);//调用Semaphore的内部类Sync的nonfairTryAcquireShared
}
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {//自旋
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
4.release解析
public void release() {
sync.releaseShared(1);//调用AQS的releaseShared
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//释放同步状态成功
doReleaseShared();//唤醒同步队列中后继结点的线程
return true;
}
return false;
}
protected boolean tryReleaseShared(int arg) {//由Semaphore的Sync实现
throw new UnsupportedOperationException();
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
* 保证释放动作(向同步等待队列尾部)传递,即使没有其他正在进行的
* 请求或释放动作。如果头节点的后继节点需要唤醒,那么执行唤醒
* 动作;如果不需要,将头结点的等待状态设置为PROPAGATE保证
* 唤醒传递。另外,为了防止过程中有新节点进入(队列),这里必
* 需做循环,所以,和其他unparkSuccessor方法使用方式不一样
* 的是,如果(头结点)等待状态设置失败,重新检测。
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;//头节点状态
// 如果头节点对应的线程是SIGNAL状态,则意味着头
//结点的后继结点所对应的线程需要被unpark-唤醒。
if (ws == Node.SIGNAL) {
// 修改头结点对应的线程状态设置为0。失败的话,则继续循环。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒头结点h的后继结点所对应的线程
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果头结点发生变化,则继续循环。否则,退出循环。
if (h == head) // loop if head changed
break;
}
}
//唤醒传入结点的后继结点对应的线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//拿到后继结点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒该线程
LockSupport.unpark(s.thread);
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {//自旋
int current = getState();//获取当前同步状态
int next = current + releases;//状态+1
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))//更新状态成功返回true
return true;
}
}
二.Exchanger
1.概念
线程可以在成对内配对和交换元素的同步点。每个线程在输入exchange方法时提供一些对象,与合作者线程匹配,并在返回时接收其合作伙伴的对象。交换器可以被视为一个的双向形式SynchronousQueue。交换器在诸如遗传算法和管道设计的应用中可能是有用的。
2.用法
Exchanger<V> 泛型类型,其中 V 表示可交换的数据类型
V exchange(V v):等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。
V exchange(V v, long timeout, TimeUnit unit):等待另一个线程到达此交换点(除非当前线程被中断或超出了指定的等待时间),然后将给定的对象传送给该线程,并接收该线程的对象。
Exchanger<Integer> exchanger = new Exchanger<>();
ExecutorService executor = Executors.newCachedThreadPool();
Runnable run = () ->{
try {
int num = new Random().nextInt(10);
System.out.println(Thread.currentThread().getName()+"开始交换数据:"+num);
num = exchanger.exchange(num);//交换数据并得到交换后的数据
System.out.println(Thread.currentThread().getName()+"交换数据结束后的数据:"+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
executor.execute(run);
executor.execute(run);
executor.shutdown();
结果:
pool-1-thread-2开始交换数据:9
pool-1-thread-1开始交换数据:8
pool-1-thread-2交换数据结束后的数据:8
pool-1-thread-1交换数据结束后的数据:9
3.exchange源码解析 参考下面的博客,写的很详细
- https://blog.csdn.net/qq_31865983/article/details/105620881
- https://www.xuebuyuan.com/2736097.html
多线程高并发编程(6) -- Semaphere、Exchanger源码分析的更多相关文章
- 多线程高并发编程(8) -- Fork/Join源码分析
一.概念 Fork/Join就是将一个大任务分解(fork)成许多个独立的小任务,然后多线程并行去处理这些小任务,每个小任务处理完得到结果再进行合并(join)得到最终的结果. 流程:任务继承Recu ...
- Java并发编程笔记之ThreadLocalRandom源码分析
JDK 并发包中 ThreadLocalRandom 类原理剖析,经常使用的随机数生成器 Random 类的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadL ...
- Java并发编程笔记之PriorityBlockingQueue源码分析
JDK 中无界优先级队列PriorityBlockingQueue 内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的? PriorityBlock ...
- Java并发编程笔记之ArrayBlockingQueue源码分析
JDK 中基于数组的阻塞队列 ArrayBlockingQueue 原理剖析,ArrayBlockingQueue 内部如何基于一把独占锁以及对应的两个条件变量实现出入队操作的线程安全? 首先我们先大 ...
- Java并发编程笔记之CopyOnWriteArrayList源码分析
并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝 ...
- Java并发编程笔记之ThreadLocal源码分析
多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候, ...
- Java并发编程中线程池源码分析及使用
当Java处理高并发的时候,线程数量特别的多的时候,而且每个线程都是执行很短的时间就结束了,频繁创建线程和销毁线程需要占用很多系统的资源和时间,会降低系统的工作效率. 参考http://www.cnb ...
- Java并发编程笔记之SimpleDateFormat源码分析
SimpleDateFormat 是 Java 提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个 SimpleDateFormat 实例对日期进行 ...
- Java并发编程笔记之AbstractQueuedSynchronizer源码分析
为什么要说AbstractQueuedSynchronizer呢? 因为AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchroni ...
随机推荐
- MRCTF 部分WriteUp
前言 周末做了一下北邮的CTF,这里记录一下做出来的几道题.(PS:比较菜有很多没做出来 >_< ,还是要更加努力学习啊(ง •̀o•́)ง,剩下的等大佬们出了wp后在复现一下) Web ...
- 改变Dataframe的列的数据类型
1.查看DataFrame的数据类型 df.dtypes#查看各列数据类型 df[A].dtypes#查看A列数据类型 2.转换DataFrame的数据类型 df[A].astypes(int)#将A ...
- effective-java学习笔记---使用接口模拟可扩展的枚举38
枚举类型( BasicOperation )不可扩展,但接口类型( Operation )是可以扩展的,并且它是用于表示 API 中的操作的接口类型. // Emulated extensible e ...
- 题解 P2620 虫洞
总体思路:离散化 + 建图 + 单源最短路(看见人少蒟蒻才敢发题解QAQ) 需要注意的是: 考虑到w范围较大,而实际虫洞数量较小,就只记录虫洞的起点与终点来建图. 建图时,虫洞起点可以去重. 在建图时 ...
- javax.el.PropertyNotFoundException: 类型[cn.cqsw.pojo.Course]上找不到属性[CourseId]
今天在JSP利用EL表达式取值报了 "javax.el.PropertyNotFoundException” 1 Caused by: org.apache.jasper.JasperExc ...
- 使用python-crontab给linxu设置定时任务
安装pip install python-crontab #coding=utf-8 from crontab import CronTab #创建类 class Crontabi(object): ...
- A 修公路
时间限制 : - MS 空间限制 : 65536 KB 评测说明 : 时限1000ms 问题描述 某岛国有n个小岛构成(编号1到n),该国政府想要通过n-1条双向公路将这些小岛连接起来,使得任意 ...
- Java并发基础05. 传统线程同步通信技术
先看一个问题: 有两个线程,子线程先执行10次,然后主线程执行5次,然后再切换到子线程执行10,再主线程执行5次--如此往返执行50次. 看完这个问题,很明显要用到线程间的通信了, 先分析一下思路:首 ...
- 1058 A+B in Hogwarts (20分)(水)
If you are a fan of Harry Potter, you would know the world of magic has its own currency system -- a ...
- python--Django从创建一个项目说起
创建项目 首先进入一个空目录,打开操作命令行,输入: django-admin startproject 项目名称 建立数据库连接 进入项目目录打开settings.py文件,修改以下字段 DATAB ...