Sentinel基本使用与源码分析
一丶什么是Sentinel
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点
,从流量路由
、流量控制
、流量整形
、熔断降级
、系统自适应过载保护
、热点流量防护
等多个维度来帮助开发者保障微服务的稳定性。
流量整形:限制流出某一网络的某一连接的流量与突发,使这类报文以比较均匀的速度向外发送。流量整形通常使用缓冲区和令牌桶来完成,当报文的发送速度过快时,首先在缓冲区进行缓存,在令牌桶的控制下再均匀地发送这些被缓冲的报文
二丶主要功能
1.流量控制
任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制
流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
2.熔断降级
当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。
为何发生雪崩:如下图中服务D奄奄一息,其他服务对它具备依赖,对服务D发起调用的时候常常超时,RT很大,导致服务G线程都block在调用服务D的这一步中,导致服务G也奄奄一息,久而久之其他服务都处于不可服务的状态。
3.系统负载保护
当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
三丶基本使用
1.sentinel依赖引入
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>版本号</version>
</dependency>
2.初始化限流规则
private static void initFlowQpsRule() {
//限流规则列表
List<FlowRule> rules = new ArrayList<FlowRule>();
//第一个规则
FlowRule rule1 = new FlowRule();
//rule1针对什么资源(资源:指你要对什么限流,一般是方法名称)
rule1.setResource(KEY);
// 设置限流阈值为20
rule1.setCount(20);
//限流策略:
// 0:线程数(那么就是20个线程并发访问的时候限流)
//1:qps:每秒访问资源的数量,超过20进行限流
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
//将受来源限制的应用程序名称
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
}
3.限流使用
Entry entry = null;
try {
entry= SphU.entry("资源名称");
//执行正常业务逻辑
} catch (BlockException e) {
//执行被限流后的业务逻辑
} finally {
if (entry != null) {
entry.close();
}
}
4.控制台设置限流规则
5.控制台设置降级规则
四丶基本原理
1.基本概念
Entry
在Sentinel中,所有的资源都对应一个资源名称以及一个 Entry。
Entry负责记录当前调用的信息:
- 创建时间:用于统计rt
- 当前节点Node:当前上下文中资源的统计信息(Node是Sentinel中的一个接口,负责记录实时统计信息)。
- 来源Node:调用来源方信息。
Slot
每一个 Entry 创建的时候,同时也会创建一系列功能插槽(slot chain)。这些插槽有不同的职责。
2.原理图
五丶源码学习
Sentinel不仅支持单机流控,还支持集群流控制,个人认为在流量分配均匀的情况下,单机流控完全够用了,并且集群流控需要额外的资源来进行集群服务器信息同步,感觉用处不是很大。
0.Sentinel SPI
一个框架需要考虑到扩展性,实现扩展性的一个很好的方式就是SPI(Service Provider Interface)SPI可与实现将装配的控制权移到程序之外,实现使用方和提供方的解耦。
Sentinel提供了SpiLoader方便进行SPI服务实现的加载,Sentinel中很多核心组件都依赖此类进行加载。常见使用如下
SpiLoader.of(xxx.class).loadInstanceListSorted()
0.1 of 方法创建SpiLoader实例
这种double check + synchronized实现线程安全的方式在Sentinel中非常常见。
0.2 SpiLoader对象加载服务提供者
SpiLoader会使用ClassLoader读取META-INF/services/服务提供者class全限定类名
文件中的数据,并解析@Spi注解中的内容,决定是否单例,是否默认实现,以及顺序等内容,加载服务实现并返回结果。
1.FlowRuleManager 管理FlowRule
- FlowRule: 表示对资源采取何种流控手段。
- FlowRuleManager:提供FlowRule的存储,查询等功能。
FlowRuleManager内部使用map来存储管理资源和对应的流控规则(一个资源可存在多个流控规则)。
另外还提供PropertyListener来实现FlowRule变化后的回调。
2.SphU.entry 进行流控
2.1 Env 调用InitFunc#init
这里会使用Env中的static final单例CtSph对象进行流控规则,并且会进行InitFunc#init的调用
2.2 包装资源为StringResourceWrapper
ResourceWrapper是对资源的包装,存在两个实现,MethodResourceWrapper在调用SphU#entry(Method method)的时候使用到。
上面原理图中提到,Sentinel具备slot链条,在获取链条的时候,会根据ResourceWrapper从缓存map中获取,hash的规则是ResourceWrapper的名称,MethodResourceWrapper的名称是:方法定义类全限定名称:方法名称(参数类型全限定类名)
。
2.3 entryWithPriority 进行流控
2.3.1 获取Context
Context保存当前调用元数据,首先会通过ContextUtil从ThreadLocal中获取,如果不存在会new一个,并且设置到ThreadLocal中,随着Entry#close方法的时候会进行资源释放。
构建Context的时候,会设置其中的Node(名称为sentinel_default_context的EntranceNode),
Node在Sentinel中用来生成树状结构,描述方法的调用
2.3.2 lookProcessChain构建Slot执行链条
slot链条式sentinel实现流量统计,和限流的核心,获取slot链条ProcessorSlot的代码如下
- 从缓存map中获取
SPI获取SlotChainBuilder
这里是一个扩展点,我们可与实现基于配置中心的chainBuilder,实现slot链条配置化
DefaultSlotChainBuilder 构造ProcessorSlotChain
ProcessorSlotChain也是一个ProcessorSlot处理器插槽,内部使用net属性串联下一个AbstractLinkedProcessorSlot。
我们可扩展自己的ProcessorSlot,使用SPI机制轻松加入到ProcessorSlotChain中。
2.3.2 依次执行ProcessorSlot#entry
1.NodeSelectorSlot
NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级
2.ClusterBuilderSlot
ClusterBuilderSlot会生成用于存储资源的统计信息以及调用者信息的ClusterNode,ClusterNode会负责存储该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据。
3.StatisticSlot
用于记录、统计不同纬度的 runtime 指标监控信息
统计请求通过数量使用了StatisticNode#addPassRequest方法
最终都是使用ArrayMetric进行记录, ArrayMetric 内部使用OccupiableBucketLeapArray或者BucketLeapArray进行计数,具体如何记录在后续章节中分析。
4.AuthoritySlot
基于白名单黑名单逻辑的权限校验,默认情况是没有启用的。
5.SystemSlot
通过系统的状态,例如 load,cpu使用率等,来控制总的入口流量。qps使用的是滑动窗口算法进行统计,load,cpu使用率这些指标通过com.sun.management.OperatingSystemMXBean获取。
6.FlowSlot
根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制。
可看到,每一个配置的FlowRule,都会调用canPassCheck检查,只要存在任何一个不满足要求,都会抛出FlowException
这里会根据FlowRule#setControlBehavior的不同选择不同的TrafficShapingController进行校验,也可以通过FlowRule#setRater直接指定的实现。
DefaultController :默认流量整形控制器,超过任何规则的阈值后,新的请求就会立即拒绝,拒绝方式为抛出
FlowException
这里代码逻辑较为简单,获取当前qps或者线程数,如果acquireCount加上当前qps大于count(阈值)那么返回false。
另外可以看到Sentinel提供了预占能力。
实现的难点在于怎么统计qps,统计线程数,这部分在后续章节中进行学习。
ThrottlingController :均速排队,严格控制请求通过的时间间隔,也即是让请求以均匀的速度通过,对应的是漏桶算法。下面是判断是否通过的代码:
private boolean checkPassUsingCachedMs(int acquireCount, double maxCountPerStat) {
//当前时间
long currentTime = TimeUtil.currentTimeMillis(); //statDurationMs = 1000ms 即1s(假设qps限制为20,maxCountPerStat=20)
//1000ms * 1(请求数量)/ maxCountPerStat = 产生1个令牌所需要耗费的时间
long costTime = Math.round(1.0d * statDurationMs * acquireCount / maxCountPerStat); // costTime + 上一个请求通过的时间 = 什么时间,此令牌可以产生
long expectedTime = costTime + latestPassedTime.get(); //如果当前时间小于 满足令牌要求的时间(expectedTime)
if (expectedTime <= currentTime) {
// Contention may exist here, but it's okay.
//设置最后通过请求的时间(latestPassedTime是AtomicLong)
latestPassedTime.set(currentTime);
return true;
} else {
// 计算等待的时间
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
//等待的时间超过了排队等待的阈值(默认情况下500ms)那么返回false
if (waitTime > maxQueueingTimeMs) {
return false;
} //上一次请求通过的时间,加上产生令牌需要的时间(addAndGet是自旋+cas操作)
long oldTime = latestPassedTime.addAndGet(costTime);
//等待时间
waitTime = oldTime - TimeUtil.currentTimeMillis();
//如果超过了等待阈值 那么cas减小时间,相当于回滚操作
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
// sleep当前线程进行等待
if (waitTime > 0) {
sleepMs(waitTime);
}
return true;
}
}
其实这个代码也不是天衣无缝:
这段代码也有很妙的地方:
WarmUpRateLimiterController/WarmUpController: 预热/冷启动方式。当系统长期处理低水平的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值的上限,给系统一个预热的时间,避免冷系统被压垮。
这部分源码设计到guava的预热算法,后续了解学习
7.DegradeSlot&DefaultCircuitBreakerSlot
二者都是熔断器插槽,并且短路原理一致,DegradeSlot在用户个资源指定DegradeRule(降级规则)的时候会根据DegradeRule构造出断路器CircuitBreaker,而DefaultCircuitBreakerSlot则是用户配置同一的短路规则,并没有给资源指定特定规则的时候,会使用默认规则生成CircuitBreaker。
DegradeRule分为三种:
- RuleConstant.DEGRADE_GRADE_RT
根据rt来进行熔断,对应ResponseTimeCircuitBreaker(响应时间断路器)
- RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO
根据异常比列进行熔断,对应ExceptionCircuitBreaker
- RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT
根据错误进行熔断,对应ExceptionCircuitBreaker
统计rt,错误次数,错误率都是基于LeapArray(滑动窗口算法)进行统计。
在DegradeSlot&DefaultCircuitBreakerSlot的entry方法(限流操作会调用到此方法)此方法会轮流调用CircuitBreaker#tryPass进行短路校验。
这里都会使用AbstractCircuitBreaker#tryPass
CircuitBreaker具备三种状态:
- Open,断路器打开,此时会判断是否大于接口恢复时间,如果大于那么修改为半开,让一个请求先试试水,如果成功那么从半开修改为开。
- Close:断路器关闭了,此时所有请求都可以通过。并且根据指标(rt,错误率,错误次数)来决定是否将断路器修改为开
- Half open,半开状态,此时除了试水的线程可以通过,其他线程都会抛出DegradeException,试水线程会根据是否调用异常,来决定修改为开还是关
可以看出断路器最重要的是维护这三种状态,并且状态的切换需要保证线程安全
如果断路器处于Open,首先会通过retryTimeoutArrived方法判断当前时间是否大于恢复时间点,如果不是,说明短路了返回false,DegradeSlot会抛出DegradeException,阻止调用。如果大于恢复时间点,会调用fromOpenToHalfOpen 尝试cas open->half_open,并且注册回调,此回调会在 entry.close()的时候被触发。
在DegradeSlot&DefaultCircuitBreakerSlot的exit方法中,会触发circuitBreaker#onRequestComplete(如果执行顺利没有出现BlockException异常的话)
这里会根据DegradeRule调用不同的CircuitBreaker
ExceptionCircuitBreaker
从Entry当中拿到执行的错误,如果具备错误,那么更新滑动窗口中的错误数
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
//当前是开,那么什么也不做,降级slot在开的状态会判断当前时间和恢复时间,实现降级效果,so,这里不需要做什么
if (currentState.get() == State.OPEN) {
return;
} //半开
if (currentState.get() == State.HALF_OPEN) {
// 半开但是执行过程中无异常,那么调整为close,说明被调用方法接口已经稳定了,可以修改为close
if (error == null) {
fromHalfOpenToClose();
} else {
//如果具备异常,那么修改为开,并且更新接口恢复时间,实现熔断
fromHalfOpenToOpen(1.0d);
}
return;
} //开状态,滑动窗口统计错误数,和总数
List<SimpleErrorCounter> counters = stat.values();
long errCount = 0;
long totalCount = 0;
//统计错误数,和总数
for (SimpleErrorCounter counter : counters) {
errCount += counter.errorCount.sum();
totalCount += counter.totalCount.sum();
}
//小于 最小请求数(默认5)认为样本太少,直接啥也不做
if (totalCount < minRequestAmount) {
return;
}
//默认根据错误次数,curCount记录错误次数
double curCount = errCount;
//根据错误比例,curCount记录错误率
if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
// Use errorRatio
curCount = errCount * 1.0d / totalCount;
}
//错误比例 or 错误率超过了阈值,那么调整为开,并且更新接口恢复时间,实现熔断
if (curCount > threshold) {
transformToOpen(curCount);
}
}
ResponseTimeCircuitBreaker
和上面类似,只不过是根据rt来判断,而不是错误次数,错误率。同使用滑动窗口实现计数。
2.4 LeapArry是如何实现滑动窗口计数的
ArrayMetirc被StatisticSlot调用addPass方法
这里的data便是LeapArray的子类BucketLeapArray,或者OccupiableBucketLeapArray
2.4.1构造方法
从构造方法我们可以看出LeapArray的构成
可以看到数据的存储使用了AtomicReferenceArray,其中sampleCount = 2,intervalInMs = 1000,也就说默认只有两个窗口,时间跨度为1s。
2.4.2 获取当前时间对应的桶
这一段就是滑动窗口的精髓
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
//根据当前时间,计算当前时间位于窗口数组中的下标
// 比如当前是 1600ms,窗口总大小为1000ms,一共两个格子,每一个格子大小为500ms
//1600ms 落在(1600/500)% 2 = 1
int idx = calculateTimeIdx(timeMillis);
// 计算当前时间 对应的起始 => 1600 - 1600%500 = 1500
// |0~500|500~1000|1000~1500|1500~2000| 1600位于1500~2000中所以起始为1500
long windowStart = calculateWindowStart(timeMillis);
while (true) {
//获取当前时间对应的桶
WindowWrap<T> old = array.get(idx);
//如果桶为null
if (old == null) {
//创建新元素
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
//cas 设置到数组中
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
//如果cas失败,说明在另外一个线程也是这个index,让当前线程yield放弃cpu
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
//旧桶的start和当前桶一样:意味着是同一秒
//比如600ms也是位于下标1,但是start 是500,就和当前1600ms的start 1500不同 意味着不是同一秒
return old;
} else if (windowStart > old.windowStart()) {
//比如600ms也是位于下标1,但是start 是500,
// 就和当前1600ms的start 1500,1500>500,意味着当前这个桶已经过期了
//上锁
if (updateLock.tryLock()) {
try {
//设置windowStart 并且清空旧桶
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
//上锁失败,那么
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
//当前start小于旧桶start
//这种情况不应该发生,因为时间时越来越来大的
//除非当前线程 一值分配不到时间片?
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
这里有一个变量windowStart,就如同一个版本,标志了元素是否过期(旧元素windowStart小于当前windowStart说明旧元素过期),是否位于一个桶(windowStart相同说明位于同一个下标,比如1600,和1700,windowStart都是1500)
2.4.3 改变桶记录的值
上面看了是如何拿到当前时间对应下标的桶的,那么桶中记录的数据是如何变更的?
可以看到这里使用了LongAdder进行计数,因为一个桶可能存在多个线程并发更新,使用LongAdder实现热点数据分离,也减少缓存伪共享带来的开销。(JUC源码学习笔记4——原子类源码分析,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法)
2.4.4 如何获取当前qps
上面我们直到了,通过的请求,根据时间对应了ArrayMetric滑动窗口数组中的一个元素,这个元素是MetricBucket类型,内部有一个LongAdder的数组,来记录pass,block(通过的请求,被拦截的请求)的数目,那么怎么根据这些数据获取当前qps昵?FlowSlot根据qps限流得使用这个数据呀!
我们看一下这里的pass方法
再看下values方法,入参就是当前时间,这个当前时间timeMills用来排除过期元素
拿通过次数自然是使用了LongAdder#sum方法,但是LongAdder#sum不具备瞬时一致性,也就说可能遍历到n位置,但是其他线程更改n-1位置,n-1的变化不会体现在sum的总和中。
这里有一段代码我觉得有点秒(个人理解,一点拙见不一定对)
就是这里统计pass会先执行data.currentWindow(),把当前时间对应的滑动窗口元素进行初始化。
比如执行data.currentWindow()当前时间点是1001ms,对应桶为null,那么这一步会初始化1001ms对应的桶元素。
后续values遍历的时间是1003ms,这时候就可以拿到1001ms初始化桶的引用返回,后续加入存在另外一个线程在1004ms进行更新,当前线程执行pass累加的时候就又更大概率统计到
为什么我说更大概率,因为如果不初始化的化,1004ms另外一个线程去初始化会new一个桶元素,这个过程是更耗时的,当前线程values遍历可能就没办法拿到1004ms这个线程初始化的桶了(有道理,但是不多doge)。
六丶总结
学到了啥:
- 学习到了SPI和责任链,这两大解耦+扩展利器。学习到了自旋+CAS的操作。
- SPI由一方制定规范,比如SpringBoot的spring.factories,另一方进行扩展,实现服务使用方和提供方的解耦合,以及修改SPI文件内容进行扩展!
- 责任链简直无处不在,在众多框架中,平时工作也常常使用到,使用责任链,责任单一,并且通过改变链条实现扩展!
- 对熔断有了更深刻的理解,特别Open状态根据恢复时间判断+cas修改为半开,让一个线程试试水的操作,我大受震撼!
- 了解到Sentinel还有除了针对单个资源进行限流,还有系统负载限流的功能,这个有点厉害,我一开始还想这个该咋实现,看了SystemSlot,发现自己还是太狭隘了。
- 学习到了SPI和责任链,这两大解耦+扩展利器。学习到了自旋+CAS的操作。
不足:
对Sentinel的Node理解不够,文档上说Sentine支持调用链路关系进行限流,这个功能挺牛的,但是我并没有详细阅读这部分源码。
对预热的实现没有深入理解,主要是Guava的预热模型,让二阳的我有点晕了,后续结合guava中的限流进行学习。
Sentinel基本使用与源码分析的更多相关文章
- alibaba sentinel限流组件 源码分析
如何使用? maven引入: <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>s ...
- 分布式缓存技术之Redis_Redis集群连接及底层源码分析
目录 1. Jedis 单点连接 2. Jedis 基于sentinel连接 基本使用 源码分析 本次源码分析基于: jedis-3.0.1 1. Jedis 单点连接 当是单点服务时,Java ...
- 2. Sentinel源码分析—Sentinel是如何进行流量统计的?
这一篇我还是继续上一篇没有讲完的内容,先上一个例子: private static final int threadCount = 100; public static void main(Strin ...
- 3. Sentinel源码分析— QPS流量控制是如何实现的?
Sentinel源码解析系列: 1.Sentinel源码分析-FlowRuleManager加载规则做了什么? 2. Sentinel源码分析-Sentinel是如何进行流量统计的? 上回我们用基于并 ...
- 4.Sentinel源码分析— Sentinel是如何做到降级的?
各位中秋节快乐啊,我觉得在这个月圆之夜有必要写一篇源码解析,以表示我内心的高兴~ Sentinel源码解析系列: 1.Sentinel源码分析-FlowRuleManager加载规则做了什么? 2. ...
- 5.Sentinel源码分析—Sentinel如何实现自适应限流?
Sentinel源码解析系列: 1.Sentinel源码分析-FlowRuleManager加载规则做了什么? 2. Sentinel源码分析-Sentinel是如何进行流量统计的? 3. Senti ...
- 6.Sentinel源码分析—Sentinel是如何动态加载配置限流的?
Sentinel源码解析系列: 1.Sentinel源码分析-FlowRuleManager加载规则做了什么? 2. Sentinel源码分析-Sentinel是如何进行流量统计的? 3. Senti ...
- 7.Sentinel源码分析—Sentinel是怎么和控制台通信的?
这里会介绍: Sentinel会使用多线程的方式实现一个类Reactor的IO模型 Sentinel会使用心跳检测来观察控制台是否正常 Sentinel源码解析系列: 1.Sentinel源码分析-F ...
- 源码分析 Sentinel 之 Dubbo 适配原理
目录 1.源码分析 SentinelDubboConsumerFilter 2.源码分析 SentienlDubboProviderFilters 3.Sentienl Dubbo FallBack ...
- 源码分析 Alibaba sentinel 滑动窗口实现原理(文末附原理图)
要实现限流.熔断等功能,首先要解决的问题是如何实时采集服务(资源)调用信息.例如将某一个接口设置的限流阔值 1W/tps,那首先如何判断当前的 TPS 是多少?Alibaba Sentinel 采用滑 ...
随机推荐
- async 与 Thread 的错误结合
在 TAP 出现之前,我们可以通过 Thread 来完成一些线程操作,从而实现多线程和异步操作.在 TAP 出现之后,有时候为了更高精度的控制线程,我们还是会使用到 Thread .文本讲介绍一种错误 ...
- JAVA重试机制多种方式深入浅出
重试机制在分布式系统中,或者调用外部接口中,都是十分重要的. 重试机制可以保护系统减少因网络波动.依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制. 为了方便说明,先假设我们想要进行 ...
- Android笔记--为活动补充附加信息
利用资源文件配置字符串 具体实现: 利用元数据传递配置信息 给应用页面注册快捷方式 Label属性,需要定义到strings.xml文件里面去:
- Javaweb学习笔记第十五弹--Listente概述、AJAX、Axiox、JSON
Listener(监听器) 可以在application.session和request三个对象创建 Javaweb提供了8个监听器,其中较为典型的是ServletContextListener监听器 ...
- 分析document文档中script标签获取抖音无水印视频
思路分析 使用 playwright 模拟浏览器打开分享链接 获取 播放页面 html 信息 解析 播放页面的 video标签,video标签的src属性就是视频的地址 这种模式会触发抖音的风控机制 ...
- java多线程---4 线程同步介绍及不安全案例说明
java多线程---4 线程同步介绍及不安全案例说明 线程同步 并发: 同一个对象被多个线程同时操作. 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步 ...
- 罗技GHUB怎么写入板载内存
本文以自用罗技MX518复刻版鼠标作为例子,让大家怎么学会把logitech G HUB的设置写入鼠标板载内存,并且一键切换各组板载设置. 首先点击最下方的启用,让软件设置鼠标各项设定 启用软件的设定 ...
- void关键字
在C++中,void表示为无类型,主要有三个用途: (1)函数的 返回值用void,表示函数没有返回值. void func(int a, int b) { //函数体代码 return; } (2) ...
- Wondershare Recovery - 万兴数据恢复专家,恢复你 Mac 上的重要文件
Wondershare Recoverit 是恢复被删除文件最有效的软件之一.计算机用户面临的一个主要问题是失去机密信息.我们可能是误删除了数据和文件,或者可能是由于病毒袭击.操作系统故障或硬盘故障而 ...
- 二进制安装Kubernetes(k8s) v1.23.6
二进制安装Kubernetes(k8s) v1.23.6 背景 kubernetes二进制安装 1.23.3 和 1.23.4 和 1.23.5 和 1.23.6 文档以及安装包已生成. 后续尽可能第 ...