Java性能 -- 字符串
实现
在Java 6以及之前的版本中,String对象是对char数组进行了封装实现的对象
主要四个成员变量:char数组、偏移量offset、字符数量count、哈希值hash
String对象通过offset和count两个属性来定位char数组,获取字符串
这样可以高效快速地共享数组对象,同时节省内存空间,但也有可能会导致内存泄露
Java 7/8,String类中不再有offset和count两个变量
这样String对象占用的内存稍微少了一点
另外,String.substring不再共享char[],从而解决了使用该方法可能导致的内存泄露问题
从Java 9开始,将char[]修改为byte[],同时维护了一个新的属性coder,它是一个编码格式的标识
一个char字符占用16位,2个字节,用一个char去存储单字节编码的字符会非常浪费
Java 9的String类为了节约内存空间,使用了占用8位,1个字节的byte数组来存放字符串
coder的作用:使用length()或者indexOf()
coder有两个默认值:0代表LATIN1,1代表UTF16
不 ...
Java并发 -- 协程
协程
协程可以理解为一种轻量级的线程
从操作系统的角度来看,线程是在内核态中调度的,而协程是在用户态调度的,协程的切换成本更低
协程栈比线程栈要小得多,典型的线程栈在1M左右,而协程栈一般在几K或者几十K左右
因此无论在时间维度还是在空间维度,协程都比线程轻量很多
支持协程的语言:Go、Python、Lua、Kotlin
Java OpenSDK的Loom项目的目标是为了支持协程
Go中的协程12345678910func hello(msg string) { fmt.Println("Hello " + msg)}func TestCoroutine(t *testing.T) { // 在新的协程中执行hello方法 go hello("Go") // 等待100毫秒让协程执行结束 time.Sleep(100 * time.Millisecond)}
Java中的线程是一个重量级对象,因此无法很好地实现Thread-Per-Message模式,而协程可以
Thread-Per-Me ...
Java并发 -- 软件事务内存
STM
STM:Software Transactional Memory,软件事务内存,借鉴于数据库的事务管理
传统的数据库事务支持ACID,即原子性(A)、一致性(C)、隔离性(I)和持久性(D)
STM不支持持久化,即只支持ACI
数据库事务数据库保证在并发情况下不会发生死锁,而且还能保证ACID
1234567891011121314Connection conn = null;try{ // 获取数据库连接 conn = DriverManager.getConnection(); // 设置手动提交事务 conn.setAutoCommit(false); // 执行转账SQL ... // 提交事务 conn.commit();} catch (Exception e) { // 出现异常回滚事务 conn.rollback();}
synchronized转账12345678910111213141516@AllArgsConstructorpublic class UnsafeAcco ...
Java性能 -- 性能调优策略
性能测试测试方法
微基准性能测试
可以精准定位到某个模块或者某个方法的性能问题,例如对比一个方法使用同步实现和非同步实现的性能差异
宏基准性能测试
宏基准性能测试是一个综合测试,需要考虑到测试环境、测试场景和测试目标
测试环境:模拟线上的真实环境
测试场景:在测试某个接口时,是否有其他业务的接口也在平行运行,进而造成干扰
测试目标
可以通过吞吐量和响应时间来衡量系统是否达标,如果不达标,就需要进行优化
如果达标,就继续加大测试的并发数,探底接口的TPS
除了关注接口的吞吐量和响应时间外,还需要关注CPU、内存和IO的使用率情况
干扰因素热身问题
在Java编程语言和环境中,.java文件编译成.class文件后,需要通过解析器将字节码转换成本地机器码才能运行
为了节约内存和执行效率,代码在最初被执行时,解析器会率先解析执行这段代码
随着代码被执行的次数增加,当JVM发现某个方法或代码块运行得很频繁时,就会把这些代码认定为热点代码
为了提高热点代码的执行效率,在运行时,JVM将通过即时编译器(JIT)把这些代码编译成与本地平台相关的机器码
并进行各层次的优化,然后存储在内存中,之后每次运行代码 ...
Java并发 -- Actor模型
Actor模型
Actor模型在本质上是一种计算模型,基本的计算单元称为Actor,在Actor模型中,所有的计算都在Actor中执行
在面向对象编程里,一切都是对象,在Actor模型里,一切都是Actor,并且Actor之间是完全隔离的,不会共享任何变量
Java本身并不支持Actor模型,如果需要在Java里使用Actor模型,需要借助第三方类库,比较完备的是Akka
Hello Actor12345678910111213141516public class HelloActor extends UntypedAbstractActor { // 该Actor在收到消息message后,会打印Hello message @Override public void onReceive(Object message) throws Throwable { System.out.printf("Hello %s%n", message); } public static void main(String[] a ...
Java性能 -- 性能调优标准
性能瓶颈
CPU
如果应用需要大量计算,会长时间占用CPU资源,导致其它应用因无法争夺到CPU而响应缓慢
场景:代码递归导致的无限循环,JVM频繁的Full GC、多线程编程造成的大量上下文切换
内存
Java程序一般通过JVM对内存进行分配管理,主要使用JVM中的堆内存来存储Java创建的对象
但内存空间有限,当内存空间被占满,对象无法回收,会导致内存溢出,内存泄露等问题
磁盘IO
网络:带宽
异常:Java应用中,抛出异常需要构建异常栈,对异常进行捕获和处理,这个过程非常消耗系统性能
数据库:数据库的操作往往涉及到磁盘IO的读写,大量的数据库读写操作,会导致磁盘IO的性能瓶颈
锁竞争
在并发编程中,经常需要使用到多线程,并发读写同一个共享资源,为了保证数据原子性,会用到锁
锁的使用会带来上下文切换,从而给系统带来性能开销
性能指标响应时间
一个接口的响应时间一般在毫秒级
数据库响应时间:数据库操作所消耗的时间,往往是整个请求链中最耗时的
服务端响应时间:包括Nginx分发请求所消耗的时间以及服务端程序执行所消耗的时间
网络响应时间:在网络传输时,网络硬件对需要传输的请求进行解析等操作 ...
Java并发 -- Disruptor
有界队列
JUC中的有界队列ArrayBlockingQueue和LinkedBlockingQueue,都是基于ReentrantLock
在高并发场景下,锁的效率并不高,Disruptor是一款性能更高的有界内存队列
Disruptor高性能的原因
内存分配更合理,使用RingBuffer,数组元素在初始化时一次性全部创建
提升缓存命中率,对象循环利用,避免频繁GC
能够避免伪共享,提升缓存利用率
采用无锁算法,避免频繁加锁、解锁的性能消耗
支持批量消费,消费者可以以无锁的方式消费多个消息
简单使用123456789101112131415161718192021222324252627public class DisruptorExample { public static void main(String[] args) throws InterruptedException { // RingBuffer大小,必须是2的N次方 int bufferSize = 1024; // 构建Disruptor D ...
Java并发 -- Netty线程模型
BIO
BIO即阻塞式IO,使用BIO模型,一般会为每个Socket分配一个独立的线程
为了避免频繁创建和销毁线程,可以采用线程池,但Socket和线程之间的对应关系不会发生变化
BIO适用于Socket连接不是很多的场景,但现在上百万的连接是很常见的,而创建上百万个线程是不现实的
因此BIO线程模型无法解决百万连接的问题
在互联网场景中,连接虽然很多,但每个连接上的请求并不频繁,因此线程大部分时间都在等待IO就绪
理想的线程模型
用一个线程来处理多个连接,可以提高线程的利用率,降低所需要的线程
使用BIO相关的API是无法实现的,BIO相关的Socket读写操作都是阻塞式的
一旦调用了阻塞式的API,在IO就绪前,调用线程会一直阻塞,也就无法处理其他的Socket连接
利用NIO相关的API能够实现一个线程处理多个连接,通过Reactor模式实现
Reactor模式
Handle指的是IO句柄,在Java网络编程里,本质上是一个网络连接
Event Handler是事件处理器,handle_event()处理IO事件,每个Event Handler处理一个IO Handle ...
Java并发 -- Guava RateLimiter
RateLimiter12345678910111213141516171819// 限流器流速:2请求/秒RateLimiter limiter = RateLimiter.create(2.0);ExecutorService pool = Executors.newFixedThreadPool(1);final long[] prev = {System.nanoTime()};for (int i = 0; i < 20; i++) { // 限流器限流 limiter.acquire(); pool.execute(() -> { long cur = System.nanoTime(); System.out.println((cur - prev[0]) / 1000_000); prev[0] = cur; });}// 输出// 499// 499// 497// 502// 496
令牌桶算法
Guava RateLimiter采用的是 ...
Java并发 -- 生产者-消费者模式
生产者-消费者模式
生产者-消费者模式的核心是一个_任务队列_
生产者线程生产任务,并将任务添加到任务队列中,消费者线程从任务队列中获取任务并执行
从架构设计的角度来看,生产者-消费者模式有一个很重要的优点:_解耦_
生产者-消费者模式另一个重要的优点是支持异步,并且能够平衡生产者和消费者的速度差异(任务队列)
支持批量执行
往数据库INSERT 1000条数据,有两种方案
第一种方案:用1000个线程并发执行,每个线程INSERT一条数据
第二种方案(更优):用1个线程,执行一个批量的SQL,一次性把1000条数据INSERT进去
将原来直接INSERT数据到数据库的线程作为生产者线程,而生产者线程只需将数据添加到任务队列
然后消费者线程负责将任务从任务队列中批量取出并批量执行
1234567891011121314151617181920212223242526272829303132333435363738// 任务队列private BlockingQueue<Task> queue = new LinkedBlockingQueue<>(2000 ...