理论上,TCP滑动窗口大小设置为带宽*单向时延*2,乘以2的原因是可以充分利用信道。
进程间通信:
- 共享内存
- 消息队列
- 信号量
- Socket
堆排序:第一步:自上而下建堆O(n);第二步:输出元素并保持有序O(logn),需要自上而下保持有序
堆插入:自下而上
逃逸分析:JVM对程序的优化,可从以下三个方面进行优化:
- 栈上分配。不会逃逸的局部变量会被分配在栈上,减少GC。
- 同步消除。不会线程逃逸的变量,会优化掉与之相关的锁。
- 标量替换。
RPC VS MQ
- RPC耦合,MQ解耦;
- MQ可削峰填谷;
- MQ可广播。
测试IO/CPU耗时比例:apm工具
1 | //LockSupport.java |
- 重复请求:幂等
- 并发请求:串行、锁
- 超量请求:扩容、限流、拒绝
- 消息乱序:锁+版本号+幂等
- 非法调用:鉴权
- 依赖不稳定:超时控制、降级、熔断
阻塞队列可以用来实现池化技术
编译时环境:*.java -> *.class(机器码)
运行时环境:*.class -> 加载进JVM
对于C/C++,在编译时选择平台
对于Java,在运行时选择平台
JDK内置工具使用jps、jstack、jmap、jstat
Java visualVM
计算机领域的所有问题都可以通过加一个中间层解决。
All problems in computer science can be solved by another level of indirection. –David Wheeler(世界上第一个计算机博士,剑桥大学)
https://en.wikipedia.org/wiki/David_Wheeler_(computer_scientist)
Java 函数式接口
函数式接口:有且仅有一个抽象方法,但可以有多个非抽象方法。
Java8新增了函数式接口,新增的类在java.util.function这个路径下。函数式接口可以被隐式转换为 lambda 表达式。在Java8引入函数式接口之前,其实Java中已经有函数式接口了,就是我们比较熟悉的Runnable和Callable。函数式接口会用注解@FunctionalInterface修饰,该注解主要作用是检查接口是否满足函数式接口的要求,即只有一个抽象方法。
| 接口 | 是否需要输入参数 | 是否有返回值 |
|---|---|---|
| Consumer | 1 | 0 |
| Function | 1 | 1 |
| Supplier | 0 | 1 |
| Predicate | 1 | Boolean |
| Runnable | 0 | 0 |
| Callable | 0 | 1 |
操作系统:
信号量模型
管程模型(管程与信号量是等价的)
并发问题的三个核心问题:分工、同步、互斥。
分工:如何高效地拆解任务并分配给线程,哪些任务可并行,哪些任务需串行,任务的分工影响着程序的并发性能;
同步:线程之间如何协作,即一个线程执行完了一个任务,如何通知下一个线程执行;
互斥:保证同一时刻只允许一个线程访问共享资源,互斥解决的是线程安全性的问题。
并发问题的根源
计算机中的CPU、内存、IO设备这三者的速度差异一直都在,为了充分榨干CPU的性能,平衡这三者的性能差异,人们采取了以下方法:
- 引入CPU缓存,平衡CPU和内存之间的速度差异;
- 对CPU进行分时复用,平衡CPU与IO设备之间的速度差异(多线程);
- 编译程序时对指令进行重排序,更加充分利用缓存。
但是这些优化措施会带来其他方面的一些问题,需要
引入缓存会带来可见性的问题,线程切换会带来原子性的问题,编译优化会带来有序性的问题。
解决这些问题的思路也比较简单,就是禁用相应的优化机制,例如要解决可见性的问题,就按需禁用缓存;解决原子性的问题,就按需禁止线程切换;解决有序性的问题,就按需禁用编译优化。
其中可见性和有序性的解决依靠的是Java内存模型,具体来说涉及到volatile、synchronized、final三个关键字以及六项Happens-Before规则。 原子性利用互斥锁解决。
Stream
Java8添加了一个新的接口Stream,该接口支持以串行或者并行的方式处理数据。
Stream将要处理的元素看作流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选、排序、聚合等。
元素流在管道中的操作分为两种类型:经过中间操作(intermediate operation)和最终操作(terminal operation)。
- 中间操作:中间操作会返回一个新的流,交给下一个操作使用,一个流后面可以跟随零个或多个中间操作。中间操作都是惰性化的(lazy),即调用中间操作时不进行执行动作,在调用最终操作时才真正开始执行动作。
- 最终操作:最终操作会返回最终的结果。一个流只能有一个最终操作,当这个操作执行后,流就被消费完了,无法再被操作。
中间操作:
- filter()
- map()
- flatMap()
- distinct()
- sorted()
- peek()
- limit()
- skip()
最终操作:
- forEach()
- forEachOrdered()
- toArray()
- reduce()
- collect()
- min()
- max()
- count()
- anyMatch()
- allMatch()
- noneMatch()
- findFirst()
- findAny()
串行与并行
1 | IntStream.range(0, 10000).forEach(list1::add);//串行 |
Java中有三种方式可以做并行:
ThreadPoolExecutor线程池
Fork/Join框架
Stream并行流
关于这三个方式的性能比较,具体可参见参考资料。
Java Stream性能测试
三种方式遍历集合:
- 显式for循环
- 串行Stream
- 并行Stream
并行Stream在任何时候好于其他两者。简单操作for循环好于串行Stream,复杂操作for循环与串行Stream相当。
https://www.cnblogs.com/secbro/p/11653574.html
https://www.cnblogs.com/CarpenterLee/p/6675568.html
Java线程是和操作系统线程一一对应的,这种做法本质上是将Java线程的调度权完全委托给操作系统,而操作系统在这方面非常成熟,所以这种做法的好处是稳定、可靠,但是也继承了操作系统线程的缺点:创建成本高。为了解决这个缺点,Java并发包里提供了线程池等工具类。
除了线程一一对应的方案外。业界还有另外一种方案:轻量级线程(协程)。这个方案在Java领域知名度并不高,但是在Go、Lua等语言中较为知名。轻量级线程的创建成本很低,基本上和创建一个普通对象的成本相似;并且创建的速度和内存占用相比操作系统线程至少有一个数量级的提升。
OpenJDK有个Loom项目,就是要解决Java轻量级线程问题,在这个项目中,轻量级线程被叫做Fiber。
在JDK中有ArrayBlockingQueue和LinkedBlockingQueue两种有界队列,它们都是基于ReentrantLock实现的,在高并发场景下,锁的效率并不高。
Disruptor是一款性能优异的有界无锁内存队列,由LMAX公司开发。 Disruptor应用广泛,在Log4j2、Spring Messaging、HBase、Storm中都有应用。
Disruptor之所以性能优异,主要有以下几点原因:
- 内存分配更加合理,使用RingBuffer数据结构,数组元素在初始化时一次性全部创建,提升缓存命中率;对象循环利用,避免频繁GC。
- 能够避免伪共享,提升缓存利用率。
- 采用无锁算法,避免频繁加锁、解锁的性能消耗。
- 支持批量消费,消费者可以无锁方式消费多个消息。
这里比较难理解的是第二点伪共享(False sharing)。伪共享会使得Cache失效。 伪共享和CPU内部的Cache有关,Cache内部是按照缓存行(Cache Line)管理的,缓存行的大小通常是64个字节。CPU从内存中加载数据X,会同时加载X后面64-size(X)个字节的数据,以填充满Cache Line。如果一行Cache Line中有多个变量,在多线程环境下,可能被预加载进来的其他变量会因为其他线程的写操作而失效,进而需要重新加载。这样的话,就起不到缓存的作用了。
Disruptor在优化并发性能方面可谓是做到了极致,优化的思路大体是两个方面:一个是利用无锁算法避免锁的争用,另外一个则是将硬件(CPU)的性能发挥到极致。尤其是后者,在Java领域基本上属于经典之作了。发挥硬件的能力一般是C这种面向硬件的语言擅长做的事,C语言领域经常通过调整内存布局优化内存占用,而Java领域则用的很少,原因在于Java可以智能地优化内存布局,内存布局对程序员是透明的。这种智能的优化大部分场景是很友好的,但是对于伪共享这种情况,就需要通过填充的方式来避免。
Java8提供了避免伪共享的注解@sun.misc.Contended,通过这个注解可以轻松避免伪共享(需要设置JVM参数 -XX:-RestrictContended),无需手动去填充。需要注意的是避免伪共享是以牺牲内存为代价的。