Striped64源码阅读

文章目录

  • 简介
  • 模型
  • 代码分析
    • 成员变量
    • 方法
  • 补充
    • ThreadLocalRandom
    • Contended注解 - 解决伪共享问题
    • LongAdder & LongAccumulator
  • 参考链接

本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习

本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么

简介

Striped64是JUC用于实现Accumulator、Adder高性能计数器的基类,比如LongAdder,在高并发的情况下性能优于AtomicLong,原因是 Striped64使用了并发隔离的技术,减少了高并发下的竞争。

Striped64对外表现为一个数字,在内部用一个base变量存储这个数字。当线程尝试更新base时,会先尝试CAS对base进行更新,如果更新失败则说明当前存在竞争,然后创建数组cells,线程此时不是重试更新base,而是去更新cells中对应的元素,最后需要获取结果的时候则将cells的所有临时结果累加到base上再返回。

相比于 Atomic 变量中所有线程竞争同一个变量,Striped64通过隔离线程的并发,让多个线程分别操作不同的变量,从而降低了竞争,减少了自旋的时间,最终提高了性能。分段并发是十分重要的减少竞争的手段,在 ConcurrentHashMap、ForkJoinPool 中也有体现。

模型

如简介所说,Striped64对外表现为一个数字,内部base就存储的是这个数字。当并发量高的时候就会导致大量对base的CAS更新失败。因此基于分段并发的思想,使用cells数组,从「每个线程都对base进行更新」,变成「每个线程对cells中对应的元素进行更新」。当外部需要获取数值时,再把cells中的每个更新结果合并到base上并返回。

具体地,cells是懒初始化的,当base更新发生竞争的时候,才会初始化cells。当cells上发生竞争,又会进一步扩容cells数组,数组的大小最多差不多为机器的CPU数量,不会无限扩容下去。cells的元素也是懒初始化的,当访问的时候才初始化。线程通过生成线程专属的hashcode定位到要访问的那个cell。当cells扩容到最大值,线程在cell上发生竞争的话,就会尝试rehash,理想状态是线程均匀hash分布到各个cell中。

我认为Striped64的思想也可以称为fork-join思想、map-reduce思想,先将一个大问题分解成各个小问题,再合并各个小问题的解得到原来大问题的解。Striped64只是在数值领域的fork-join,理解好Striped64,有利于以后理解更加通用和复杂的fork-join模型。

代码分析

成员变量

// CPU个数,可视为最大并行数
static final int NCPU = Runtime.getRuntime().availableProcessors();
// cells本身和元素都是懒初始化的。cells大小为2的幂
transient volatile Cell[] cells;
transient volatile long base;
// 基于CAS的锁,0为不加锁,1为加锁,当cells扩容或者创建Cell时加锁
transient volatile int cellsBusy;

cells数组的元素类型Cell:

@jdk.internal.vm.annotation.Contended static final class Cell {// Cell其实就是对该value的封装volatile long value;Cell(long x) { value = x; }final boolean cas(long cmp, long val) {return VALUE.weakCompareAndSetRelease(this, cmp, val);}final void reset() {VALUE.setVolatile(this, 0L);}final void reset(long identity) {VALUE.setVolatile(this, identity);}final long getAndSet(long val) {return (long)VALUE.getAndSet(this, val);}// 获取value的VarHandle,以支持weakCompareAndSetReleaseprivate static final VarHandle VALUE;static {try {MethodHandles.Lookup l = MethodHandles.lookup();VALUE = l.findVarHandle(Striped64.Cell.class, "value", long.class);} catch (ReflectiveOperationException e) {throw new ExceptionInInitializerError(e);}}
}

Cell可以看成是简易版的AtomicLong,只支持volatile读和release版本的CAS(即weakCompareAndSetRelease,关于release的含义可看这篇)。

另外还涉及到Thread类的成员变量:

// 本线程对于ThreadLocalRandom的探针哈希值
@jdk.internal.vm.annotation.Contended("tlr")
int threadLocalRandomProbe;

这个threadLocalRandomProbe就是Striped64中线程要映射到cells所使用的hashcode,与Object.hashcode不同,这个threadLocalRandomProbe是个随机数(为了与Object.hashcode区分开,在下文称其为probe),并且可以动态变化(rehash),在Striped64中通过以下两个方法读取 或 更新:

// 获取probe
static final int getProbe() {return (int) THREAD_PROBE.get(Thread.currentThread());
}// rehash,通过传入的probe产生新的probe
// 如果传入的probe是0,rehash结果也为0,因此必须先初始化线程的probe
static final int advanceProbe(int probe) {probe ^= probe << 13;   // xorshiftprobe ^= probe >>> 17;probe ^= probe << 5;THREAD_PROBE.set(Thread.currentThread(), probe);return probe;
}

目前了解到这里就够了,主要还是先分析Striped64的实现思路,在文末再做相关的补充。

方法

Striped64分别对long和double类型各有一套方法处理,由于base、cell等都是默认用long,因此double值以IEEE 754双精度浮点数的方式存储在long类型的变量中并且在运行时进行转换(因为double和long都是64位所以可以这样存),除了数据转换外处理逻辑是一模一样的,看long版本的longAccumulate就行

这个函数虽然叫xxxAccumulate,意味着将x加到base上,默认是"+"运算,其实还可以传入fn,自定义x和base之间的运算:

// wasUncontended:false表示调用之前对cell的CAS失败了
// index:线程probe在cells中对应的下标
final void longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended, int index) {// 如果下下标为0,可能是probe还没初始化(为0)// 因此用ThreadLocalRandom.current()为probe初始化if (index == 0) {ThreadLocalRandom.current();index = getProbe();wasUncontended = true;}// 使用无限循环方便CAS重试。collide=true表示多线程竞争同一个cellfor (boolean collide = false;;) {// 变量说明:// cs: cells// c: 本线程对应的cell// n: cells容量// v: cell.value或baseStriped64.Cell[] cs; Striped64.Cell c; int n; long v;// 分支1. 使用cells进行操作if ((cs = cells) != null && (n = cs.length) > 0) {// 如果发现没有cell,那么为线程创建cellif ((c = cs[(n - 1) & index]) == null) {if (cellsBusy == 0) {Striped64.Cell r = new Striped64.Cell(x);// 加锁if (cellsBusy == 0 && casCellsBusy()) {try {Striped64.Cell[] rs; int m, j;if ((rs = cells) != null &&(m = rs.length) > 0 &&rs[j = (m - 1) & index] == null) { // double-checkrs[j] = r;break;}} finally {cellsBusy = 0;}continue;}}collide = false;}// 否则,线程有对应cell,如果曾经CAS失败过,那么就不急着对该cell进行CAS// 而是进行下面的double hashing,让线程映射到别的cellelse if (!wasUncontended)wasUncontended = true;// 否则,没有进行过CAS,尝试对cell进行CASelse if (c.cas(v = c.value,(fn == null) ? v + x : fn.applyAsLong(v, x)))break; // 成功,退出// 否则,CAS失败,说明有其他线程更新同一个cell。// 如果已经达到最大容量或者cells已过时(被其他线程扩容或丢弃),// 那么不视为冲突(因为冲突的话,本线程会扩容cells),重新开始循环else if (n >= NCPU || cells != cs)collide = false;// 否则,标记为冲突,由下一轮循环来扩容else if (!collide)collide = true;// 加锁扩容else if (cellsBusy == 0 && casCellsBusy()) {try {if (cells == cs) // double-check是否过时cellscells = Arrays.copyOf(cs, n << 1); // 扩容为原来的两倍} finally {cellsBusy = 0;}collide = false;continue;}// double hashing: 每次retry都生成新probe,可能为了增加随机性啥的吧index = advanceProbe(index);}// 分支2. 初始化cells并为线程创建一个cellelse if (cellsBusy == 0 && cells == cs && casCellsBusy()) {try {if (cells == cs) { // double-checkStriped64.Cell[] rs = new Striped64.Cell[2]; // 初始化容量为2rs[index & 1] = new Striped64.Cell(x); // 创建cellcells = rs;break;}} finally {cellsBusy = 0;}}// 分支3. 加锁失败,说明cells正在被初始化/修改,只能对base动手了else if (casBase(v = base, (fn == null) ? v + x : fn.applyAsLong(v, x)))break;}
}

看过JUC其他类的源码的小伙伴知道,longAccumulator的代码结构类似于AQS中的acquire,外面套一个无限循环,循环里面if…else if多个分支,运行流程可以看成一个有限状态机:根据当前某些变量的值选择进入对应的分支,然后进入下一个状态或终态…

循环内分为三个大分支,可以看出longAccumulate是优先使用操作cells而不是base的,至于为什么,可以到LongAdder.add方法看一下,他是对base更新失败才调用longAccumulate的,甚至cell存在的话还会对cell尝试更新一下(这就是为什么要传一个wasUncontended的原因)

结合代码中的注释,实现流程如下:

  • 分支1. 如果cells存在,那么使用cells进行操作
    • 如果没有cell则创建cell
    • 如果曾经对cell的CAS失败过,就取消这个失败的标记
    • 对cell进行CAS
    • 如果CAS失败则表示cell发生冲突,扩容(可能会由于过时cells或已达最大容量无法扩容)
  • 分支2. 否则cells不存在,那么加锁并初始化cells
  • 分支3. 否则加锁失败,此时也不会傻傻空转,而是尝试去直接更新base

分支1的最后会对probe进行double hashing。

补充

ThreadLocalRandom

之前说到线程有一个“探针哈希值”(Probe hash value),每个线程都拥有自己的探针哈希值,并用他作为hashcode定位到cells的下标。线程的探针哈希值是作为Thread成员变量存储的:

// 本线程的探针哈希值
@jdk.internal.vm.annotation.Contended("tlr")
int threadLocalRandomProbe;

那么这个哈希值threadLocalRandomProbe(以下简称为probe)是如何计算得到的?这与类ThreadLocalRandom有关。首先这个类是用于生成随机数,而同样能生成随机数的java.util.Random虽然是线程安全的,但由于多线程共用会导致性能降低,因此ThreadLocalRandom通过实现类似ThreadLocal那样的线程隔离机制(内部并没有使用ThreadLocal)来提高多线程下并发生成随机数的性能。

更进一步地,对Random的竞争其实是对随机数种子发生的竞争,因此ThreadLocalRandom实现方式是为每个线程维护各自的随机数种子,这样就能用各自的种子生成随机数互不干涉。

首先这个类的一般使用方式是:

// 单例模式
// 必须在需要获取随机数的那个线程内调用current获取单例
ThreadLocalRandom.current().nextInt();

为什么要求在线程内获取单例呢?看一下current的实现:

public class ThreadLocalRandom extends Random {private static final ThreadLocalRandom instance = new ThreadLocalRandom();// 获取ThreadLocalRandom单例public static ThreadLocalRandom current() {// 判断线程的threadLocalRandomProbe是否为0,如果为0的话表示还没为线程初始化种子// 调用localInit进行初始化if (U.getInt(Thread.currentThread(), PROBE) == 0)localInit();return instance;}// 初始化线程的种子和probestatic final void localInit() {// 生成probe,如果生成的probe恰好为0的话则设为1int p = probeGenerator.addAndGet(PROBE_INCREMENT);int probe = (p == 0) ? 1 : p;// 生成随机数种子long seed = RandomSupport.mixMurmur64(seeder.getAndAdd(SEEDER_INCREMENT));// 设置线程的种子和probeThread t = Thread.currentThread();U.putLong(t, SEED, seed);U.putInt(t, PROBE, probe);}
}

哦,原来设置线程的随机数种子是在获取单例时进行的,并且有没有设置种子是通过probe是否为0来判断的。线程的随机数种子与probe一样,也是Thread类的成员变量:

@jdk.internal.vm.annotation.Contended("tlr")
long threadLocalRandomSeed;

获取ThreadLocalRandom单例后,调用nextInt生成随机数:

public int nextInt() {return mix32(nextSeed());
}final long nextSeed() {Thread t; long r; // read and update per-thread seedU.putLong(t = Thread.currentThread(), SEED,r = U.getLong(t, SEED) + (t.getId() << 1) + GOLDEN_GAMMA);return r;
}

nextInt通过操作这个线程的种子来生成随机数,由于线程操作的是各自的种子,因此多线程生成随机数不发生竞争。

这样一看,probe好像也就用来判断一下有没有初始化种子而已。其实它的另一个作用早已说了:用作线程的探针哈希值。比如在Striped64中,用作线程的哈希值,定位到其在cells中的下标。(在ForkJoin、ConcurrentHashMap中也有类似的作用)。什么叫探针哈希值?而且既然是哈希值为什么不直接用Object.hashcode方法?说直白点,探针哈希值就是可以动态改变的哈希值,当多线程在同一个数据单元发生冲突的时候,可以通过更新probe使其定位到其他的数据单元,避免冲突,即通过rehash避免冲突。

总结一下,这个小节主要说明了threadLocalRandomProbe的由来和用途,顺带提了一下ThreadLocalRandom这个类。threadLocalRandomProbe的用途有两个:

  • 判断线程是否已经初始化随机数种子和probe
  • 用作线程的探针哈希值(专门用于多线程并发,并不与Thread的什么属性有关联)

关于ThreadLocalRandom和Striped64最后还有一些东西额外补充下:

  • Striped64中的getProbeadvanceProbe方法与ThreadLocalRandom的这两个同名方法实现是一样的,但是由于后者位于j.u.c包下并且方法是package-private的,所以位于j.u.c.atomic包的Striped64访问不了,只能复制过来用:

    Duplicated from ThreadLocalRandom because of packaging restrictions.

  • 我发现JDK 17+的ThreadLocalRandom类中有几个生成随机数的方法比如nextInt(int bound)直接调用了super.nextInt(bound),即Random.nextInt,这样岂不是失去了线程隔离的特性,又回到了竞争随机数种子的问题。关于这点我在stackoverflow提了一个问题,并且很快得到了回复:https://stackoverflow.com/questions/77917763/jdk-21-why-threadlocalrandoms-nextint-implement-with-super-nextint-directly

Contended注解 - 解决伪共享问题

细心的小伙伴可能会注意到cells数组的元素类型Cell有一个注解:@jdk.internal.vm.annotation.Contended,这个注解有什么用呢?首先得了解「伪共享」的概念。

我们知道,计算机使用了cache缓存提高访问内存的效率。在SMP架构的处理器上,为每个CPU核单独设置一个cache,以提高读写并行访问效率。而CPU对外需要表现为只有一个cache,即表现为各个cache的数据应该是相同的,因此解决cache一致性问题,即修改了一个cache的数据时,需要同步到其他cache上,否则其他核将读到旧的数据。并且这个同步是按cache行为单位的,cache行大小一般为64B。比如核修改了其cache中某行的几个字节,其他核访问其cache的同一行就会检测到该行已失效,并需要从主存中重新读取最新数据(最新的cache需要先将该写到主存)。

在Java中,不严谨地说,访问变量就是访问该变量所在的cache行。而小于64B的多个变量可能会放在同一个cache行中,比如:

class Obj {public volatile long a;public volatile long b;
}

Obj实例的a和b两个变量由于在内存上相邻存放,很可能会加载到同一个cache行中。当两个线程分别读写a和b变量,并且假设两个线程绑定在了不同的CPU核上并行运行:

// Thread 1
a = 1; // 在Thread 2读b之前写入// Thread 2
int x = b;

此时,虽然两个线程访问的是不同变量,在Java的层面上并没有共享变量,但实际上两个变量共享了同一个cache line,并导致:当Thread 2读b的时候,a所在的cache line需要先刷到内存,然后再从内存读到Thread 2所在核对应的cache,这就造成了「伪共享」现象,即两个看起来互不相干的变量实际上共享了cache line。

在JDK 7之前,可以在可能发生伪共享的变量前后加上填充字节(padding),效果上是使该变量占据整个cache line,这样a所在的cache line就永远不会出现b的身影:

class Obj {private volatile long p0, p1, p2, p3, p4, p5, p6; // 64b * 7public volatile long a;                           // 64bprivate volatile long p0, p1, p2, p3, p4, p5, p6; // 64b * 7public volatile long b;
}

JDK 8则提供了Contented注解达到类似的效果,使得不用手写padding:

class Obj {@jdk.internal.vm.annotation.Contendedpublic volatile long a;@jdk.internal.vm.annotation.Contendedpublic volatile long b;
}

之前说过的probe和随机数种子也是(在JDK 21中去掉了这里的Contented,估计是后来的实现不会出现伪共享):

public class Thread implements Runnable {@jdk.internal.vm.annotation.Contended("tlr")long threadLocalRandomSeed;@jdk.internal.vm.annotation.Contended("tlr")int threadLocalRandomProbe;
}

再回到Striped64的Cell类上,cells本来就是让每个线程访问不同的元素减少共享以提高并发下的性能,但数组的元素在内存上是相邻存放的,很容易出现伪共享现象,反而增加了访存次数降低性能,因此将数组元素类型Cell加上Contented注解,使每个元素占据独立的cache line,避免伪共享。

LongAdder & LongAccumulator

LongAdder和LongAccumulator的基类都是Striped64,前者顾名思义就是基于"+“运算符的long类型计数器,后者则是基于自定义运算符的long类型计数器,如果运算符定义为”+",那么将与LongAdder没有区别。具体实现就不分析了,自己看看就行非常简单。下面看看他们使用时需要注意的问题。

LongAdder就不说了,没有什么需要注意的。

LongAccumulator需要注意的是,传入LongAccumulator的自定义函数(自定义运算符)必须满足交换律,比如"+"是满足交换律的。否则就会出现意想不到的结果,比如:

var accumulator = new LongAccumulator((v, x) -> { v*2+x }, 0); // 定义运算符为base*2 + x// 1.
accumulator.accumulate(1); // 0*2+1 = 1
accumulator.accumulate(2); // 1*2+2 = 4// 2.
accumulator.accumulate(2); // 0*2+2 = 2
accumulator.accumulate(1); // 2*2+1 = 5

上面的1、2两个运算,由于两次accumulate的顺序不同,导致最终结果分别为4和5。多线程跑的时候,各个accumulate的顺序就更加不能确定,所以肯定不能这么玩。

另外需要注意的是reset函数:

public void reset() {Striped64.Cell[] cs = cells;base = identity; // base设为初始值if (cs != null) {for (Striped64.Cell c : cs)if (c != null)c.reset(identity); // cell也设为初始值}
}

这个函数用于将LongAccumulator重置为构造时传入的初始值,但迷惑的点在于它将所有的cell也重置为这个值,这样会导致一些问题。比如运算符定义为"+",初始值为100,开100个线程,对LongAccumulator加到100,检查结果为110,然后调用reset再次检查结果,得到的值会比初始值10大:

LongAccumulator accumulator = new LongAccumulator(Long::sum, 10); // 初始值为10var ex = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {ex.execute(() -> accumulator.accumulate(1)); // 加100次1
}
ex.shutdown();
ex.awaitTermination(10, TimeUnit.SECONDS);System.out.println(accumulator.get()); // 输出110
accumulator.reset();
System.out.println(accumulator.get()); // 输出30

经过分析Striped64后,这个例子最终输出30的原因我们都知道,但就是不知道LongAccumulator为什么要这么设计,即cell也设置为初始值。

所以,LongAccumulator有点傻逼,最好别用或者了解原理了后小心点用。

参考链接

「Java并发知识」Striped64

「简书」Java 并发计数组件Striped64详解

「简书」Java8使用@sun.misc.Contended避免伪共享

「StackOverflow」LongAccumulator does not get a right result

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/669553.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

6.s081 学习实验记录(五)traps

文章目录 一、RISC-V assembly简介问题 二、Backtrace简介注意实验代码实验结果 三、Alarm简介注意实验代码实验结果 一、RISC-V assembly 简介 git checkout traps&#xff0c;切换到traps分支user/call.c 文件在我们输入 make fs.img 之后会被汇编为 call.asm 文件&#xf…

网络原理TCP/IP(5)

文章目录 IP协议IP协议报头地址管理网段划分特殊的IP地址路由选择以太网认识MAC地址对比理解MAC地址和IP地址DNS&#xff08;域名服务器&#xff09; IP协议 IP协议主要完成的工作是两方面&#xff1a; 地址管理&#xff0c;使用一套地址体系&#xff0c;来描述互联网上每个设…

day20网页基本标签

网页基本标签 标题标签段落标签换行标签水平线标签字体样式标签注释和特殊符号 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>网页基本标签</title> </head> <body> <!--标题…

CTFshow web(php特性 105-108)

web105 <?php /* # -*- coding: utf-8 -*- # Author: Firebasky # Date: 2020-09-16 11:25:09 # Last Modified by: h1xa # Last Modified time: 2020-09-28 22:34:07 */ highlight_file(__FILE__); include(flag.php); error_reporting(0); $error你还想要flag嘛&…

BVH动画绑骨蒙皮并在Unity上展示

文章目录 Blender绑定骨骼Blender蒙皮Blender中导入bvh文件将FBX导入Unity Blender绑定骨骼 先左上角红框进入model模式&#xff0c;选中要绑定的模型&#xff0c;然后进入Edit模式把骨骼和关节对齐。 &#xff08;选中骨骼&#xff0c;G移动&#xff0c;R旋转&#xff09; 为…

如何使用NimExec通过无文件命令执行实现横向移动

关于NimExec NimExec是一款功能强大的无文件远程命令执行工具&#xff0c;该工具专为红队研究人员设计&#xff0c;使用Nim语言开发&#xff0c;基于服务控制管理器远程协议&#xff08;MS-SCMR&#xff09;实现其功能&#xff0c;可以帮助广大研究人员在目标网络系统中实现横…

婚姻是什么哩?

我们应该明白&#xff0c;或许以后也会明白。婚姻应该是爱情的结晶&#xff0c;而不是父母亲戚的催促。当你遇到一个人&#xff0c;一个合适的、合拍的人&#xff0c;你自然而然就会有想结婚的想法。遇到一个能随时发起聊天&#xff0c;聊起来能忽视时间的人&#xff0c;真的会…

谈谈mybatis的理解(一)

mybatis不允许方法的重载&#xff0c;因为ID不能重复 mybatis 为什么要使用mybatis? JDBC的弊端&#xff1a; 硬编码&#xff1a;SQL语句存在Java代码中&#xff0c;不能很好的分离数据库语句和Java语句&#xff0c;造成代码不易维护 代码重复度高&#xff1a;大量重复的…

C++:哈希表的线性探测(模拟实现)

哈希表的增删查改的效率很高&#xff0c;是O&#xff08;1&#xff09;&#xff0c;比搜索二叉树要快很多。那么他是怎么实现的呢&#xff1f;他与计数排序有点相似就是通过映射的方式实现。不过在哈希表中不需要开这么的数据&#xff0c;它只需要开一部分空间然后使用除留余数…

【算法与数据结构】583、72、LeetCode两个字符串的删除操作+编辑距离

文章目录 一、583、两个字符串的删除操作二、72、编辑距离三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、583、两个字符串的删除操作 思路分析&#xff1a;本题的思路和115、不同的子序列差不多&#xff0c;只是变成…

【Java EE初阶十】多线程进阶二(CAS等)

1. 关于CAS CAS: 全称Compare and swap&#xff0c;字面意思:”比较并交换“&#xff0c;且比较交换的是寄存器和内存&#xff1b; 一个 CAS 涉及到以下操作&#xff1a; 下面通过语法来进一步进项说明&#xff1a; 下面有一个内存M&#xff0c;和两个寄存器A,B; CAS(M,A,B)&am…

minio怎么创建bucket

在使用docker-compose安装的MinIO环境中创建bucket&#xff08;存储桶&#xff09;通常涉及到使用MinIO的客户端工具mc&#xff08;MinIO Client&#xff09;。以下是如何使用mc来创建一个名为ability-bucket的bucket的步骤&#xff1a; 步骤 1: 下载并配置mc客户端 下载mc&am…

比较Kamailio和OpenSIPS的重写contact函数

Kamailio&#xff1a;调用set_contact_alias()之后&#xff0c;在原有的contact的后面增加参数&#xff0c;具体地说&#xff0c;就是网络地址&#xff0c;网络端口和transport&#xff0c;好处是收到后续请求之时可以恢复原有contact的内容 OpenSIPS&#xff1a;调用fix_nate…

synchronized内部工作原理

作者简介&#xff1a; zoro-1&#xff0c;目前大二&#xff0c;正在学习Java&#xff0c;数据结构&#xff0c;javaee等 作者主页&#xff1a; zoro-1的主页 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f496; synchronized内部工作原理 syn…

矿泉水市场调研:预计2029年将达到83亿美元

矿泉水为国民饮水消费升级的方向&#xff0c;估算我国矿泉水市场规模约472亿元&#xff0c;成长性好。我们按照水种将包装水划分为矿泉水、纯净水、天然水及其他&#xff0c;根据多个第三方数据来源数据&#xff0c;我们估算矿泉水2017年瓶装与桶装合计市场销售规模约472亿元&a…

深入理解指针(3)

⽬录 1. 字符指针变量 2. 数组指针变量 3. ⼆维数组传参的本质 4. 函数指针变量 5. 函数指针数组 6. 转移表 1. 字符指针变量 在指针的类型中我们知道有⼀种指针类型为字符指针 char* ; ⼀般使⽤: int main() {char ch w;char *pc &ch;*pc w;return 0; } 还有…

属性“xxxx”在类型“ArrayConstructor”上不存在。是否需要更改目标库? 请尝试将 “lib” 编译器选项更改为“es2015”或更高版本。

使用vscode编写vue&#xff0c;在使用elementUI时&#xff0c;发现代码中的form报错如下&#xff1a; 属性“form”在类型“ArrayConstructor”上不存在。是否需要更改目标库? 请尝试将 “lib” 编译器选项更改为“es2015”或更高版本。 解决方法&#xff1a; 打开jsconfig.…

Springboot拦截器+redis实现暴力请求拦截

在实际项目开发部署过程中&#xff0c;我们需要保证服务的安全性和可用性&#xff0c;当项目部署到服务器后&#xff0c;就要考虑服务被恶意请求和暴力攻击的情况。如何防止我们对外的接口被暴力攻击&#xff1f;下面的教程&#xff0c;通过Springboot提供的拦截器和Redis 针对…

快速掌握Vue.js框架:从入门到实战

一、引言 Vue.js,作为一款广受欢迎的渐进式JavaScript框架,以其轻量级、易用性和高效性在前端开发领域占据了一席之地。Vue.js遵循MVVM(Model-View-ViewModel)设计模式,它通过双向数据绑定机制简化了开发者对用户界面与底层数据模型之间关系的处理,使得构建现代Web应用变…

TOP100-二叉数

1.94. 二叉树的中序遍历 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2]示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[]示例 3&#xff1a; 输入&#xf…