Java核心技术36讲笔记(2)String优化、动态代理、IO复制、线程状态以及注意点

一 String、StringBuffer、StringBuilder

  • String是一个不可变(Imutable)对象
  • StringBuffer线程安全
  • StringBuilder线程不安全(速度快)

1.1 StringBuffer选择合适的初始化大小

为了实现修改字符序列的目的, StringBuffer和StringBuilder底层都是利用可修改的( char ,JDK 9以后是byte )数组,二者都继承了AbstractStringBuilder ,里面包含了基本操作,区别仅在于最终的方法是否加了synchronized.

这个内部数组应该创建成多大的呢?
如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy.

1.2 自动编译优化

在这里插入图片描述
你可以看到,在JDK 8中,字符串拼接操作会自动被javac转换为StringBuilder操作,而在JDK 9里面则是因为Java 9为了更加统一字符串操作优化 ,提供了StringConcatFactory ,作为一个统一的入口。javac 自动生成的代码,虽然未必是最优化的,但普通场景也足够了,你可以酌情选择。

二 动态代理原理

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装RPC调用、面向切面的编程( AOP )
实现动态代理的方式很多,比如JDK自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似ASM、cglib (基于ASM )、Javassist 等。

2.1 spring中的两种代理方式

JDK Proxy的优势

  • 最小化依赖关系,减少依赖意味着简化开发和维护, JDK本身的支持,可能比cglib更加可靠。(不用外部依赖)
  • 平滑进行JDK版本升级,而字节码类库通常需要进行更新以保证在新版Java.上能够使用。
  • 代码实现简单。

基于类似cglib框架的优势

  • 有的时候调用目标可能·不便实现额外接口·,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似cglib动态代理就没有这种限制。
  • 只操作我们关心的类,而不必为其他相关类增加工作量。
  • 高性能。

性能对比

在主流JDK版本中, JDKProxy在典型场景可以提供对等的性能水平,数量级的差距基本上不是广泛存在的。而且,反射机制性能在现代JDK中,自身已经得到了极大的改进和优化,同时, JDK很多功能也不完全是反射,同样使用了ASM进行字节码(jvm中inflation)操作。

我们在选型中性能未必是唯一考量,可靠性、可维护性、编程工作量等往往是更主要的考虑因素,毕竟标准类库和反射编程的槛要低得多,代码量也是更加可控的,如果我们比较下不同开源项目在动态代理开发上的投入,也能看到这一-点。

三 IO复制

3.1 普通本地IO操作

当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。写入操作也是类似,仅仅是步骤相反。
在这里插入图片描述

3.2 nio使用零拷贝

基于NIO transferTo的实现方式,在Linux和Unix(需要看底层操作系统是否支持)上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。
注意, transferTo不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行Socket发送,同样可以享受这种机制带来的性能和扩展性提高。transferTo的传输过程是:
在这里插入图片描述

四 直接缓冲区

  • Direct Buffer :如果我们看Buffer的方法定义,你会发现它定义了isDirect() 方法,返回当前Buffer是否是Direct类型。这是因为Java提供了堆内和堆外( Direct ) Buffer ,我们可以以它的allocate或allocateDirect 方法直接创建。
  • MappedByteBuffer :它将文件按照指定大小直接映射为内存区域,当程序访问这个内存区域时将直接操作这块儿文件数据,省去了将数据从内核空间向用户空间传输的损耗。我们可以使FileChannel.map创建MappedByteBuffer ,它本质上也是种Direct Buffer(本质是一样的).

4.1 优势与适用场景

  • Direct Buffer生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多I0操作会很高效
  • 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高。
    但是请注意, Direct Buffer创建和销毁过程中,都会比一般的堆内Buffer增加部分开销,所以通常都建议用于长期使用、数据较大的场景。

4.2 回收建议

  • 在应用程序中,显式地调用System.gc()来强制触发。
  • 另外一种思路是,在大量使用Direct Buffer的部分框架中,框架会自己在程序中调用释放方法, Netty就是这么做的,有兴趣可以参考其实现( PlatformDependentO )。
  • 重复使用Direct Buffer.

五 线程状态

网上的资料对线程状态的分类有很多种,但在java api中明确只有5种

关于线程生命周期的不同状态,在Java 5以后,线程状态被明确定义在其公共内部枚举类型java.lang.Thread.State 中,分别是:

  1. 新建(NEW) ,表示线程被创建出来还没真正启动的状态,可以认为它是个Java内部状态
  2. 就绪( RUNNABLE) , 表示该线程已经在JVM中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它CPU片段,在就绪队列里面排队。在其他一些分析中,会额外区分一种状态RUNNING,但是从Java API的角度,并不能表示出来。
  3. 阻塞( BLOCKED ) , 这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待
    Monitor lock。比如,线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。
  4. 等待( WAITING) , 表示正在等待其他线程采取某些操作。一个个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待( wait) , 另外的生产者线程去准备任务数据,然后通过类似notify等动作,通知消费线程可以继续工作了。Thread.join()也会令线程进入等待状态。
    • 计时等待( TIMED_ WAIT ) , 其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如wait或join等方法的指定超时版本
  5. 终止( TERMINATED) , 不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。

在这里插入图片描述

5.1 注意线程无故唤醒场景

在这里插入图片描述

六 守护线程

有的时候应用中需要一个长期驻留的服务程序 ,但是不希望其影响应用退出,就可以将其设置为守护线程,如果JVM发现只有守护线程存在时,将结束进程,具体可以参考下面代码段。注意,必须在线程启动之前设置

t1.setDaemon(true);

七 慎用ThreadLocal

ThreadLocal使用完,需要调用remove方法显示清除value。防止内存泄漏

特别是配合线程池一起使用时,因为线程池的线程会服用。如果没有remove,很可能一直回收不了value

发布了107 篇原创文章 · 获赞 1 · 访问量 3940

猜你喜欢

转载自blog.csdn.net/m0_38060977/article/details/104240745