为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。
例如:CPU高速缓存。尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存以提高性能。
例如:运行时指令重排。当CPU写缓存时发现缓存区正被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行。
想要实现多个线程之间的协同,涉及到线程之间的相互通信。JDK提供了线程协同的API,细分为suspend/resume、wait/notify、park/unpark。suspend/resume由于容易写出死锁的代码,已经被弃用。
线程是不是越多越好?
1、 线程在java中是一个对象,更是操作系统的资源,线程创建、销毁需要时间。如果创建时间+销毁时间>执行任务时间 就很不合算。
2、 java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认最大栈大学1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。
3、 操作系统需要频繁切换线程上下文,影响性能。
线程池的推出,就是为了方便控制线程数量。
如何确定合适数量的线程?
计算型任务:CPU数量的1-2倍;
IO型任务:相对比计算型任务,需要多一些线程,要根据具体的IO阻塞时长进行考量决定。
java内存模型描述了运行多线程程序的合法行为。
可见性问题:让一个线程对共享变量的修改,能够及时被其他线程看到。
volatile关键字有如下功能:
1、禁止缓存、
2、对volatile变量相关的指令不做重排序。
有些处理器没有提供写单个字节的功能。在这样的处理器上更新byte数组,若只是简单地读取整个内容,更新对应的字节,然后将整个内容在写回内存,将是不合法的。
这个问题有时候被称为“字分裂(word tearing)”,更新单个字节有难度的处理器,就要寻求其它方式来解决问题。
因此,编程人员需要注意,尽量不要对byte[]中的元素进行重新赋值,更不要在多线程程序中这样做。
《java语言规范》中说道:建议程序员将共享的64位值(long、double)用volatile修饰或正确同步其程序以避免可能的复杂情况。
CAS的三个问题
1、 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。
2、 仅针对单个变量的操作,不能用于多个变量来实现原子操作。
3、 ABA问题。
只有当多个线程更新共享资源时,才会发生竞态条件,可能会出现线程安全问题。
栈封闭时,不会在线程之间共享变量,都是线程安全的。
局部对象引用本身不共享,但是引用的对象存储在共享堆中。如果方法内创建的对象,只是在方法中传递,并且不对其他线程可用,那么也是线程安全的。
不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。实例被创建,value变量就不能再被修改,这就是不可变性。
使用Threadlocal时,相当于不同的线程操作的是不同的资源,所以不存在线程安全问题。
Synchronized
优点: 1、使用简单,语义清晰。
2、由JVM提供,提供了多种优化方案(锁粗化、锁消除、偏向锁、轻量级锁)
3、锁的释放由虚拟机来完成,不用人工干预,也降低了死锁的可能性
缺点:无法实现一些锁的高级功能如:公平锁、中断锁、超时锁、读写锁、共享锁等。
Lock
优点: 1、所有synchronized的缺点
2、可以实现更多的功能
缺点:需手动释放锁unlock,新手使用不当可能造成死锁
概念:维护一对关联锁,一个只用于读操作,一个只用于写操作;读锁可以由多个读线程同时持有,写锁是排他的。同一时间,两把锁不能被不同线程持有。
适用场景:适合读取操作多于写入操作的场景,改进互斥锁的性能,比如:集合的并发线程安全性改造、缓存组件
BIO的缺点:阻塞导致在处理网络I/o时,一个线程只能处理一个网络连接。
NIO中有三个核心组件:Buffer缓存区、Channel通道、Selector选择器。
在文章《Scalable IO in Java》中,介绍了使用NIO与Reactor模式相结合来实现高伸缩性网络的模型。在这个模型中,mianReactor负责处理client的请求;subReactor可以有多个,每个subReactor都会在一个独立线程中执行。
因为网络编程本身的复杂性,以及JDK API开发的难度较高,所以在开源社区中,涌出来很多对JDK NIO进行封装、增强后的网络编程框架,例如:Netty、Mina等。
Netty是一个高性能、高可扩展性的异步事件驱动的网络应用程序框架,它极大地简化了TCP和UDP客户端和服务器开发等网络编程。
Netty重要的四个内容:
1、Reactor线程模型:一种高性能的多线程程序设计思路。
2、Netty中自己定义的Channel概念:增强版的通道概念。
3、ChannelPipeline职责链设计模式:事件处理机制,保证了Netty的高度可扩展性。
4、内存管理:增强的ByteBuf缓冲区
核心三个要点:线程模型、职责链、ByteBuf。
1、业务上的耗时操作,通过单独创建线程池进行处理,不要占用宝贵I/O线程。
2、Pipeline机制,有一项需要优化:handler复用。
3、Netty本身就是有很多优化 – 例如ByteBuf(零拷贝、对象复用、内存复用)。
4、大数据数据写入, 分成多批次,小数据包的方式。
垃圾收集器:
1、串行收集器 - Serial GC -XX:+UseSerialGC
单个线程来执行所有垃圾收集工作,适合单处理器机器。
2、串行收集器 - Serial Old -XX:+UseSerialOldGC
可以在老年代使用,它采用了标记-整理算法,区别于新生代的复制算法。
3、并行收集器 - Parallel GC -XX:+UseParallelGC
并行收集器 - Parallel Old GC -XX:+UseParallelOldGC
server模式JVM的默认GC选择,整体算法和Serial比较相似,区别是新生代和老年代GC都是并行进行;也称为吞吐量优先的GC。
4、并发收集器 - CMS GC -XX:+UseConcMarkSweepGC
专用老年代,基于标记-清除算法,设计目标是尽量减少停顿时间。减少了停顿时间,这一点对于互联网web等对时间敏感的系统非常重要。
5、并行收集器 - ParNew GC -XX:+UseParNewGC
新生代GC实现,它实际是Serial GC的多线程版本。最常见的应用场景是配合老年代的CMS GC工作。
6、并发收集器 - G1 -XX:+UseG1GC
针对大堆内存设计的收集器,兼顾吞吐量和停顿时间,JDK9后为默认选项,目标是替代CMS。
调优基本概念
在调整性能时,JVM有三个组件:
1、堆大小调整
2、垃圾收集器调整
3、JIT编译器
大多数调优选项都与调整堆大小和为您的情况选择最合适的垃圾收集器有关。JIT编译器对性能也有很大影响,但很少需要使用较新版本的JVM进行调优。
通常,在调优Java应用程序时,重点是以下两个主要目标之一:
响应性:应用程序或系统对请求的数据进行响应的速度,对于专注于响应性的应用程序,长的暂停时间是不可接受的,重点是在短时间内做出回应。
吞吐量:侧重于在特定时间段内最大化应用程序的工作量,对于专注于吞吐量的应用程序,高暂停时间是可接受的。由于高吞吐量应用程序在较长时间内专注于基准测试,因此不需要考虑快速响应时间。
GC调优思路
1、分析场景
2、确定目标
3、收集日志
4、分析日志
5、调整参数
线程数调为多少合适?
理想的线程数量=(1+代码阻塞时间/代码执行时间)*CPU数量
实际情况是跑起代码,压测环境进行调试。不断调整线程数,将CPU打到80-90%的利用率。