41.Java实现并发的几种方式
1)synchronized 保证一次只有一个线程在执行代码块
2)volatile 保证任何线程在读取volatile修饰的变量,读取到的都是这个变量的最新数据
3)多线程Thread、线程池
4)CompletableFuture 实现一个可无需等待被调用函数的返回值而让操作继续运行的方法
42.cms vs g1
CMS
以获取最短回收停顿时间为目标的收集器,基于并发标记清理实现。
1)绝大部分新生成的对象都放在Eden区,当Eden区将满时触发Minor GC ,进行Eden区+有对象的Survivor区垃圾回收,把存活的对象用复制算法拷贝到另一个空的Survivor中。
2)若发现Survivor区满了或者某些对象足够Old(每进行一次Minor GC,对象年龄+1 ),将这部分对象拷贝到Old区。
3)Old区会进行Major GC (至少伴随一次Minor GC,效率比Minor GC慢十倍以上)。
4)JVM在Old区申请不到内存,会进行Full GC。
优势:
并发,低停顿
劣势:
1)对CPU非常敏感,会因为占用了一部分线程使应用程序变慢
2)无法处理浮动垃圾,并发清理过程中用户线程执行也会产生垃圾,只有等到下一次gc的时候才能清理掉。
3)CMS使用标记-清理法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦。
G1
是一款面向服务端应用的垃圾收集器,从JDK 9开始G1替代并行垃圾回收器成为JVM中默认的垃圾回收器。
1)当新生代占用达到一定比例的时候,开始出发收集。(从根集合开始扫描能直接达到的对象)
2)新生代的区域(region)经过Minor GC后,存活的对象被复制到一个或者多个区域空闲中,这些被填充的区域将是新的新生代。当新生代对象的年龄(Minor GC后年龄增加1)已经达到某个阈值(默认15),被复制到老年代的区域中。
3)复制过程是把源内存分段中所有存活的对象复制到空的目标内存分段上,复制完成后,源内存分段没有了存活对象,变成了可以使用的空的Eden内存分段了。而目标内存分段的对象都是连续存储的,没有碎片。Humongous区用于保存大对象,如果一个对象占用的空间超过内存分段的一半(比如上面的8M),则此对象将会被分配在Humongous区。如果对象的大小超过一个甚至几个分段的大小,则对象会分配在物理连续的多个Humongous分段上。Humongous对象因为占用内存较大并且连续会被优先回收。
优势:
1)G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU来缩短stop-The-World停顿时间。部分其他收集器需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2)虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果,G1可以自己管理新生代和老年代了。
3)G1将整个Java堆划分为多个大小相等的独立区域(Region),每个内存分段都可以被标记为Eden区,Survivor区,Old区和Humongous区。虽然保留了新生代和来年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,G1运作期间不会产生内存空间碎片。
4)G1建立可预测的停顿时间模型,可以设置最大GC停顿时间(GC pause time)指标。(JVM 会尽量去达成这个目标)
劣势:
1)region 大小和大对象很难保证一致,这会导致空间的浪费。特别大的对象是可能占用超过一个 region 的,在分配大对象时更难找到连续空间。
43.为什么不推荐通过Executors直接创建线程池,推荐通过ThreadPoolExecutor方式创建?
通过Executors的方法创建出来的线程池都实现了ExecutorSerivice接口,其中
1)newFixedThreadPool和newSingleThreadExecutor
堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool
线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
ThreadPoolExecutor可以通过以下参数设置
corePoolSize:核心池的大小,没有任务需要执行的时候线程池的大小,只有在工作队列满了的情况下才会创建超出这个数量的线程。
maximumPoolSize:最大线程数,如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。
keepAliveTime:当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit:keepAliveTime 的时间单位。threadFactory:线程工厂。
workQueue:用来储存等待执行任务的队列。
1)ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
2)LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
3)PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
4)DelayQueue: 使用优先级队列实现的无界阻塞队列。
5)SynchronousQueue: 不存储元素的阻塞队列。
6)LinkedTransferQueue: 由链表结构组成的无界阻塞队列。
7)LinkedBlockingDeque: 由链表结构组成的双向阻塞队列。
handler :拒绝策略。
1)ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
2)ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
4)ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
44.Java 性能瓶颈分析经历
1)用top命令查看java应用是否负载高
jstack 线程id > log,将java线程dump出来
将pid列的数字转换成16进制,在log中搜索对应16进制的pid,查看线程信息
2)工具:Jmeter、jvisualvm、JMX、nmon
可以在JAVA进程启动的时候,添加如下几个参数(开启JMX):
Dcom.sun.management.jmxremote.port=7969 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=xx.xx.xx.xx(服务器的IP
地址)
启动jvisualvm,
添加JMX
配置,利用nmon
观察应用服务器CPU
的负载情况。
提高tps,查看cpu是否存在上涨停滞的情况,观察dump文件查看是否有锁或其他问题,是否存在频繁gc
45.常见设计模式应用
1)单例
保证私有化构造函数,只能有一个实例对象存在,避免频繁的创建与销毁。
2)构建者
当创建一个类的过程比较复杂时(例如要组合对象、以及判断构造参数是否足够和合法),用专门的类(如建立一个专门的Builder类)和方法将这个创建的过程封装起来。
3)工厂方法
给方法传入类的名称,方法给你返回你想要的类实例。
4)抽象工厂
一个抽象工厂类,可以派生出多个具体工厂类。
5)观察者
业务处理结束,异步发送通知消息
46.分布式调度
1)quartz
依赖mysql,多个节点部署,只能有一个节点抢到数据库锁。
没有管理界面,且不支持任务分片等。
2)elastic-job-cloud
通过zookeeper的注册与发现,可以动态的添加服务器,支持水平扩容。
需要引入zookeeper , mesos, 增加系统复杂度。
3)xxL-job
支持弹性扩容,可以分片广播和故障转移。Rolling实时日志,任务进度监控。
调度中心通过获取 DB锁来保证集群中执行任务的唯一性,集群数量过多,数据库的锁竞争会比较厉害,影响性能。
47.tcp三次握手、四次挥手,滑动窗口
三次握手
第一次:Client先产生一个初始序列号Seq,作为SYN并将该数据包发送给Server。
第二次:Server收到数据包后也发送自己的SYN报文作为响应,并初始化序列号Seq ,为了确认Client的Seq,Server将Client发送的Seq加1,作为ACK发送给Client。
第三次:为了确认Server的SYN,Client将Server发送的Seq加1,作为ACK发送给Server,完成三次握手。
四次挥手
第一次:Client发送一个FIN,用来关闭Client到Server的数据传输。
第二次:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1。
第三次:Server发送一个FIN,用来关闭Server到Client的数据传输。
第四次:Client收到FIN后,接着发送一个ACK给Server,确认序号为收到序号+1,完成四次挥手。
滑动窗口
TCP会话的双方都各自维护一个发送窗口
和一个接收窗口
。
发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界。
接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保对端会对这些数据重传。