Java知识梳理八

一、Java多线程三

1.Java后台明显变慢的诊断思路
  • 服务是突然变慢还是长时间运行后观察到变慢?类似问题是否重复出现?
  • “慢”的定义是什么,我能够理解是系统对其他方面的请求的反应延时变长吗?第二,理清问题的症状,这更便于定位具体的原因,有以下一些思路:
  • 问题可能来自于Java服务自身,也可能仅仅是受系统里其他服务的影响。初始判断可以先确认是否出现了意外的程序错误,例如检查应用本身的错误日志。
    对于分布式系统,很多公司都会实现更加系统的日志、性能等监控系统。一些Java诊断工具也可以用于这个诊断,例如通过JFR([Java FlightRecordera>),监控应用是否大量出现了某种类型的异常。 如果有,那么异常可能就是个突破点。 如果没有,可以先检查系统级别的资源等情况,监控CPU、内存等资源是否被其他进程大量占用,并且这种占用是否不符合系统正常运行状况。
  • 监控Java服务自身,例如GC日志里面是否观察到Full GC等恶劣情况出现,或者是否Minor GC在变长等;利用jstat等工具,获取内存使用的统计信息也是个常用手段;利用jstack等工具检查是否出现死锁等。
  • 如果还不能确定具体问题,对应用进行Profiling也是个办法,但因为它会对系统产生侵入性,如果不是非常必要,大多数情况下并不建议在生产系统进行。
  • 定位了程序错误或者JVM配置的问题后,就可以采取相应的补救措施,然后验证是否解决,否则还需要重复上面部分过程。
2.Lambda能让Java程序慢30倍

       第一,基准测试是一个非常有效的通用手段,让我们以直观、量化的方式,判断程序在特定条件下的性能表现。
       第二,基准测试必须明确定义自身的范围和目标,否则很有可能产生误导的结果。前面代码片段本身的逻辑就有瑕疵,更多的开销是源于自动装箱、拆箱(auto-boxing/unboxing),而不是源自Lambda和Stream,所以得出的初始结论是没有说服力的。
       第三,虽然Lambda/Stream为Java提供了强大的函数式编程能力,但是也需要正视其局限性:

  • 一般来说,我们可以认为Lambda/Stream提供了与传统方式接近对等的性能,但是如果对于性能非常敏感,就不能完全忽视它在特定场景的性能差异了,例如: 初始化的开销 。 Lambda并不算是语法糖,而是一种新的工作机制,在首次调用时,JVM需要为其构建CallSite实例。这意味着,如果Java应用启动过程引入了很多Lambda语句,会导致启动过程变慢。其实现特点决定了JVM对它的优化可能与传统方式存在差异。
  • 增加了程序诊断等方面的复杂性,程序栈要复杂很多,Fluent风格本身也不算是对于调试非常友好的结构,并且在可检查异常的处理方面也存在着局限性等。
3.JVM优化Java代码都做了什么?

       JVM在对代码执行的优化可分为运行时(runtime)优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如TLAB)等。除此之外,还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。
       JVM的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序运行profile的投机性优化(speculative/optimistic optimization)。这个怎么理解呢?比如我有一条instanceof指令,在编译之前的执行过程中,测试对象的类一直是同一个,那么即时编译器可以假设编译之后的执行过程中还会是这一个类,并且根据这个类直接返回instanceof的结果。如果出现了其他类,那么就抛弃这段编译后的机器码,并且切换回解释执行。
       当然,JVM的优化方式仅仅作用在运行应用代码的时候。如果应用代码本身阻塞了,比如说并发时等待另一线程的结果,这就不在JVM的优化范畴啦。

4.MySQL的事务和锁

       所谓隔离级别(Isolation Level),就是在数据库事务中,为保证并发数据读写的正确性而提出的定义,它并不是MySQL专有的概念,而是源于ANSI/ISO制定的SQL-92标准。每种关系型数据库都提供了各自特色的隔离级别实现,虽然在通常的定义中是以锁为实现单元,但实际的实现千差万别。以最常见的MySQL
InnoDB引擎为例,它是基于 [MVCC](https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-
versioning.html)(Multi-Versioning Concurrency Control)和锁的复合实现,按照隔离程度从低到高,MySQL事务隔离级别分为四个不同层次:

  • 读未提交(Read uncommitted),就是一个事务能够看到其他事务尚未提交的修改,这是最低的隔离水平,允许脏读出现。
  • 读已提交(Read committed),事务能够看到的数据都是其他事务已经提交的修改,也就是保证不会看到任何中间性状态,当然脏读也不会出现。读已提交仍然是比较低级别的隔离,并不保证再次读取时能够获取同样的数据,也就是允许其他事务并发修改数据,允许不可重复读和幻象读(Phantom Read)出现。
  • 可重复读(Repeatable reads),保证同一个事务中多次读取的数据是一致的,这是MySQL InnoDB引擎的默认隔离级别,但是和一些其他数据库实现不同的是,可以简单认为MySQL在可重复读级别不会出现幻象读。
  • 串行化(Serializable),并发事务之间是串行化的,通常意味着读取需要获取共享读锁,更新需要获取排他写锁,如果SQL使用WHERE语句,还会获取区间锁(MySQL以GAP锁形式实现,可重复读级别中默认也会使用),这是最高的隔离级别。
           至于悲观锁和乐观锁,也并不是MySQL或者数据库中独有的概念,而是并发编程的基本概念。主要区别在于,操作共享数据时,“悲观锁”即认为数据出现冲突的可能性更大,而“乐观锁”则是认为大部分情况不会出现冲突,进而决定是否采取排他性措施。反映到MySQL数据库应用开发中,悲观锁一般就是利用类似SELECT … FOR UPDATE这样的语句,对数据加锁,避免其他事务意外修改数据。乐观锁则与Java并发包中的AtomicFieldUpdater类似,也是利用CAS机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。
           MVCC的质就可以看作是种乐观锁机制,而排他性的读写锁、双阶段锁等则是悲观锁的实现。有关它们的应用场景,你可以构建一下简化的火车余票查询和购票系统。同时查询的人可能很多,虽然具体座位票只能是卖给一个人,但余票可能很多,而且也并不能预测哪个查询者会购票,这个时候就更适合用乐观锁。
    更多MySQL的事务和锁参见博客
5.Spring Bean的生命周期

       Spring Bean生命周期比较复杂,可以分为创建和销毁两个过程。首先,创建Bean会经过一系列的步骤,主要包括:

  • 实例化Bean对象。
  • 设置Bean属性。
  • 如果我们通过各种Aware接口声明了依赖关系,则会注入Bean对容器基础设施层面的依赖。具体包括BeanNameAware、BeanFactoryAware和ApplicationContextAware,分别会注入Bean ID、Bean Factory或者ApplicationContext。
  • 调用BeanPostProcessor的前置初始化方法postProcessBeforeInitialization。
  • 如果实现了InitializingBean接口,则会调用afterPropertiesSet方法。
  • 调用Bean自身定义的init方法。
  • 调用BeanPostProcessor的后置初始化方法postProcessAfterInitialization。
  • 创建过程完毕。你可以参考下面示意图理解这个具体过程和先后顺序。
    image

           第二,Spring Bean的销毁过程会依次调用DisposableBean的destroy方法和Bean自身定制的destroy方法。Spring Bean有五个作用域,其中最基础的有下面两种:

  • Singleton,这是Spring的默认作用域,也就是为每个IOC容器创建唯一的一个Bean实例。
  • Prototype,针对每个getBean请求,容器都会单独创建一个Bean实例。从Bean的特点来看,Prototype适合有状态的Bean,而Singleton则更适合无状态的情况。另外,使用Prototype作用域需要经过仔细思考,毕竟频繁创建和销毁Bean是有明显开销的。如果是Web容器,则支持另外三种作用域:
  • Request,为每个HTTP请求创建单独的Bean实例。
  • Session,很显然Bean实例的作用域是Session范围。
  • GlobalSession,用于Portlet容器,因为每个Portlet有单独的Session,GlobalSession提供一个全局性的HTTP Session。
6.Netty如何实现高性能?

       单独从性能角度,Netty在基础的NIO等类库之上进行了很多改进。例如:

  • 更加优雅的Reactor模式实现、灵活的线程模型、利用EventLoop等创新性的机制,可以非常高效地管理成百上千的Channel。
  • 充分利用了Java的Zero-Copy机制,并且从多种角度,“斤斤计较”般的降低内存分配和回收的开销。例如,使用池化的Direct Buffer等技术,在提高IO性能的同时,减少了对象的创建和销毁;利用反射等技术直接操纵SelectionKey,使用数组而不是Java容器等。
  • 使用更多本地代码。例如,直接利用JNI调用Open SSL等方式,获得比Java内建SSL引擎更好的性能。
  • 在通信协议、序列化等其他角度的优化。
           总的来说,Netty并没有Java核心类库那些强烈的通用性、跨平台等各种负担,针对性能等特定目标以及Linux等特定环境,采取了一些极致的优化手段。
7.常用分布式ID的设计方案

       首先,我们需要明确通常的分布式ID定义,基本的要求包括:

  • 全局唯一,区别于单点系统的唯一,全局是要求分布式系统内唯一。
  • 有序性,通常都需要保证生成的ID是有序递增的。例如,在数据库存储等场景中,有序ID便于确定数据位置,往往更加高效。
           目前业界的方案很多,典型方案包括:
  • 基于数据库自增序列的实现。这种方式优缺点都非常明显,好处是简单易用,但是在扩展性和可靠性等方面存在局限性。
  • 基于Twitter早期开源的Snowflake的实现,以及相关改动方案。这是目前应用相对比较广泛的一种方式,其结构定义你可以参考下面的示意图。
    image

           整体长度通常是64 (1 + 41 + 10+ 12 = 64)位,适合使用Java语言中的long类型来存储。头部是1位的正负标识位。紧跟着的高位部分包含41位时间戳,通常使用System.currentTimeMillis()。后面是10位的WorkerID,标准定义是5位数据中心 + 5位机器ID,组成了机器编号,以区分不同的集群节点。最后的12位就是单位毫秒内可生成的序列号数目的理论极限。Snowflake的官方版本是基于Scala语言,Java等其他语言的参考实现有很多,是一种非常简单实用的方式,具体位数的定义是可以根据分布式系统的真实场景进行修改的,并不一定要严格按照示意图中的设计。
  • Redis、Zookeeper、MongoDB等中间件,也都有各种唯一ID解决方案。其中一些设计也可以算作是Snowflake方案的变种。例如,MongoDB的ObjectId提供了一个12 byte(96位)的ID定义,其中32位用于记录以秒为单位的时间,机器ID则为24位,16位用作进程ID,24位随机起始的计数序列。
  • 国内的一些大厂开源了其自身的部分分布式ID实现,InfoQ就曾经介绍过微信的seqsvr,它采取了相对复杂的两层架构,并根据社交应用的数据特点进行了针对性设计,具体请参考相关代码实现。另外,百度、美团等也都有开源或者分享了不同的分布式ID实现,都可以进行参考。

你可能感兴趣的:(Java知识梳理八)