JDK源码中一些实用的“小技巧”总结

前言

这段时间比较闲,就看起了jdk源码。一般的一个高级开发工程师, 能阅读一些源码对自己的提升还是蛮大的。本文总结了一些JDK源码中的“小技巧”,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

1 i++ vs i--

String源码的第985行,equals方法中

 while (n--!= 0) {
  if (v1[i] != v2[i])
   return false;
  i++;
 }

这段代码是用于判断字符串是否相等,但有个奇怪地方是用了i--!=0来做判断,我们通常不是用i++么?为什么用i--呢?而且循环次数相同。原因在于编译后会多一条指令:

i-- 操作本身会影响CPSR(当前程序状态寄存器),CPSR常见的标志有N(结果为负), Z(结果为0),C(有进位),O(有溢出)。i > 0,可以直接通过Z标志判断出来。

i++操作也会影响CPSR(当前程序状态寄存器),但只影响O(有溢出)标志,这对于i < n的判断没有任何帮助。所以还需要一条额外的比较指令,也就是说每个循环要多执行一条指令。

简单来说,跟0比较会少一条指令。所以,循环使用i--,高端大气上档次。

2 成员变量 vs 局部变量

JDK源码在任何方法中几乎都会用一个局部变量来接受成员变量,比如

public int compareTo(String anotherString) {
  int len1 = value.length;
  int len2 = anotherString.value.length;

因为局部变量初始化后是在该方法线程栈中,而成员变量初始化是在堆内存中,显然前者更快,所以,我们在方法中尽量避免直接使用成员变量,而是使用局部变量。

3 刻意加载到寄存器 && 将耗时操作放到锁外部

在ConcurrentHashMap中,锁segment的操作很有意思,它不是直接锁,而是类似于自旋锁,反复尝试获取锁,并且在获取锁的过程中,会遍历链表,从而将数据先加载到寄存器中缓存中,避免在锁的过程中在便利,同时,生成新对象的操作也是放到锁的外部来做,避免在锁中的耗时操作

  final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    /** 在往该 segment 写入前,需要先获取该 segment 的独占锁
      不是强制lock(),而是进行尝试 */
    HashEntry<K,V> node = tryLock() ? null :
      scanAndLockForPut(key, hash, value);

scanAndLockForPut()源码

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
  HashEntry<K,V> first = entryForHash(this, hash);
  HashEntry<K,V> e = first;
  HashEntry<K,V> node = null;
  int retries = -1; // negative while locating node

  // 循环获取锁
  while (!tryLock()) {
    HashEntry<K,V> f; // to recheck first below
    if (retries < 0) {
      if (e == null) {
        if (node == null) // speculatively create node
          //该hash位无值,新建对象,而不用再到put()方法的锁中再新建
          node = new HashEntry<K,V>(hash, key, value, null);
        retries = 0;
      }
      //该hash位置key也相同,退化成自旋锁
      else if (key.equals(e.key))
        retries = 0;
      else
        // 循环链表,cpu能自动将链表读入缓存
        e = e.next;
    }
    // retries>0时就变成自旋锁。当然,如果重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁
    //  lock() 是阻塞方法,直到获取锁后返回,否则挂起
    else if (++retries > MAX_SCAN_RETRIES) {
      lock();
      break;
    }
    else if ((retries & 1) == 0 &&
         // 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头
         //   所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法
         (f = entryForHash(this, hash)) != first) {
      e = first = f; // re-traverse if entry changed
      retries = -1;
    }
  }
  return node;
}

4 判断对象相等可先用==

在判断对象是否相等时,可先用==,因为==直接比较地址,非常快,而equals的话会最对象值的比较,相对较慢,所以有可能的话,可以用a==b || a.equals(b)来比较对象是否相等

5 关于transient

transient是用来阻止序列化的,但HashMap源码中内部数组是定义为transient的

 /**
   * The table, resized as necessary. Length MUST Always be a power of two.
   */
  transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

那岂不里面的键值对都无法序列化了么,网络中用hashmap来传输岂不是无法传输,其实不然。

Effective Java 2nd, Item75, Joshua大神提到:

For example, consider the case of a hash table. The physical
representation is a sequence of hash buckets containing key-value
entries. The bucket that an entry resides in is a function of the hash
code of its key, which is not, in general, guaranteed to be the same
from JVM implementation to JVM implementation. In fact, it isn't even
guaranteed to be the same from run to run. Therefore, accepting the
default serialized form for a hash table would constitute a serious
bug. Serializing and deserializing the hash table could yield an
object whose invariants were seriously corrupt.

怎么理解? 看一下HashMap.get()/put()知道, 读写Map是根据Object.hashcode()来确定从哪个bucket读/写. 而Object.hashcode()是native方法, 不同的JVM里可能是不一样的.

打个比方说, 向HashMap存一个entry, key为 字符串"STRING", 在第一个java程序里, "STRING"的hashcode()为1, 存入第1号bucket; 在第二个java程序里, "STRING"的hashcode()有可能就是2, 存入第2号bucket. 如果用默认的串行化(Entry[] table不用transient), 那么这个HashMap从第一个java程序里通过串行化导入第二个java程序后, 其内存分布是一样的, 这就不对了.

举个例子,比如向HashMap存一个键值对entry, key="方老司", 在第一个java程序里, "方老司"的hashcode()为1, 存入table[1],好,现在传到另一个在JVM程序里, "方老司" 的hashcode()有可能就是2, 于是到table[2]去取,结果值不存在。

HashMap现在的readObject和writeObject是把内容 输出/输入, 把HashMap重新生成出来.

6 不要用char

char在Java中utf-16编码,是2个字节,而2个字节是无法表示全部字符的。2个字节表示的称为 BMP,另外的作为high surrogate和 low surrogate 拼接组成由4字节表示的字符。比如String源码中的indexOf:

 //这里用int来接受一个char,方便判断范围
 public int indexOf(int ch, int fromIndex) {
    final int max = value.length;
    if (fromIndex < 0) {
      fromIndex = 0;
    } else if (fromIndex >= max) {
      // Note: fromIndex might be near -1>>>1.
      return -1;
    }
    //在Bmp范围
    if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
      // handle most cases here (ch is a BMP code point or a
      // negative value (invalid code point))
      final char[] value = this.value;
      for (int i = fromIndex; i < max; i++) {
        if (value[i] == ch) {
          return i;
        }
      }
      return -1;
    } else {
      //否则转到四个字节的判断方式
      return indexOfSupplementary(ch, fromIndex);
    }
  }

所以Java的char只能表示utf­16中的bmp部分字符。对于CJK(中日韩统一表意文字)部分扩展字符集则无法表示。

例如,下图中除Ext-A部分,char均无法表示。

此外还有一种说法是要用char,密码别用String,String是常量(即创建之后就无法更改),会保存到常量池中,如果有其他进程可以dump这个进程的内存,那么密码就会随着常量池被dump出去从而泄露,而char[]可以写入其他的信息从而改变,即是被dump了也会减少泄露密码的风险。

但个人认为你都能dump内存了难道是一个char能够防范的住的?除非是String在常量池中未被回收,而被其它线程直接从常量池中读取,但恐怕也是非常罕见的吧。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 通过JDK源码角度分析Long类详解

    概况 Java的Long类主要的作用就是对基本类型long进行封装,提供了一些处理long类型的方法,比如long到String类型的转换方法或String类型到long类型的转换方法,当然也包含与其他类型之间的转换方法.除此之外还有一些位相关的操作. Java long数据类型 long数据类型是64位有符号的Java原始数据类型.当对整数的计算结果可能超出int数据类型的范围时使用. long数据类型范围是-9,223,372,036,854,775,808至9,223,372,036,85

  • 解决调试JDK源码时,不能查看变量的值问题

    前几天本来想以debug模式看一下JDK的源码,进入调试模式时才发现,根本看不到方法里面变量值的情况.为什么呢?JDK现在的版本中,编译过后,去除了里面的调试信息.解决办法是,编译那些类,使其带有调试信息,使用命令:javac -g 查看了一些相关资料,现将解决方法放到下面 1.在d:\的根目录下创建jdk7_src和jdk_debug目录. 2.在JDK_HOME目录下找到src.zip文件,并把它里面的文件解压到jdk7_src目录下,然后在解压后的目录中删除除了java.javax.org

  • Java从JDK源码角度对Object进行实例分析

    Object是所有类的父类,也就是说java中所有的类都是直接或者间接继承自Object类.比如你随便创建一个classA,虽然没有明说,但默认是extendsObject的. 后面的三个点"..."表示可以接受若干不确定数量的参数.老的写法是Objectargs[]这样,但新版本的java中推荐使用...来表示.例如 publicvoidgetSomething(String...strings)(){} object是java中所有类的父类,也就是说所有的类,不管是自己创建的类还是

  • 通过JDK源码学习InputStream详解

    概况 本文主要给大家介绍了通过JDK源码学习InputStream的相关内容,JDK 给我们提供了很多实用的输入流 xxxInputStream,而 InputStream 是所有字节输入流的抽象.包括 ByteArrayInputStream .FilterInputStream .BufferedInputStream .DataInputStream 和 PushbackInputStream 等等.下面话不多说了,来一起看看详细的介绍吧. 如何阅读JDK源码. 以看核心虚拟机(hotsp

  • 解决java 查看JDK中底层源码的实现方法

    1.点 "window"-> "Preferences" -> "Java" -> "Installed JRES"2.此时"Installed JRES"右边是列表窗格,列出了系统中的 JRE 环境,选择你的JRE,然后点边上的 "Edit...", 会出现一个窗口(Edit JRE)3.选中rt.jar文件的这一项:"c:\program files\ja

  • jdk源码阅读Collection详解

    见过一句夸张的话,叫做"没有阅读过jdk源码的人不算学过java".从今天起开始精读源码.而适合精读的源码无非就是java.io,.util和.lang包下的类. 面试题中对于集合的考察还是比较多的,所以我就先从集合的源码开始看起. (一)首先是Collection接口. Collection是所有collection类的根接口;Collection继承了Iterable,即所有的Collection中的类都能使用foreach方法. /** * Collection是所有collec

  • 通过JDK源码分析关闭钩子详解

    关闭钩子 用户关闭关闭程序,需要做一些善后的清理工作,但问题是,某些用户不会按照推荐的方法关闭应用程序,肯能导致善后工作无法进行.像tomcat调用server的start方法启动容器,然后会逐级调用start.当发出关闭命令是会启动关闭功能,但是关闭可能会有一些意外产生,导致应用程序没有进入到我们制定的关闭方法去.如何解决这个问题呢,使得即使有意外也能正常进入关闭流程. 好在java提供了一种优雅的方式去解决这种问题.使得关闭的善后处理的代码能执行.java的关闭钩子能确保总是执行,无论用户如

  • JDK源码之PriorityQueue解析

    一.优先队列的应用 优先队列在程序开发中屡见不鲜,比如操作系统在进行进程调度时一种可行的算法是使用优先队列,当一个新的进程被fork()出来后,首先将它放到队列的最后,而操作系统内部的Scheduler负责不断地从这个优先队列中取出优先级较高的进程执行:爬虫系统在执行时往往也需要从一个优先级队列中循环取出高优先级任务并进行抓取.可以想见,如果类似这样的任务不适用优先级进行划分的话,系统必会出现故障,例如操作系统中低优先级进程持续占用资源而高优先级进程始终在队列中等待.此外,优先队列在贪婪算法中也

  • JDK源码中一些实用的“小技巧”总结

    前言 这段时间比较闲,就看起了jdk源码.一般的一个高级开发工程师, 能阅读一些源码对自己的提升还是蛮大的.本文总结了一些JDK源码中的"小技巧",分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 1 i++ vs i-- String源码的第985行,equals方法中 while (n--!= 0) { if (v1[i] != v2[i]) return false; i++; } 这段代码是用于判断字符串是否相等,但有个奇怪地方是用了i--!=0来做判断,我们通

  • 最通俗的白话讲解JDK源码中的ThreadLocal

    目录 引言 ThreadLocal是什么?它能干什么? ThreadLocal实现线程隔离的秘密 为什么ThreadLocal会出现OOM的问题? 内存泄漏演示 内存泄漏问题分析 父子线程的参数传递 总结 引言 其实网上有很多关于ThreadLocal的文章了,有不少文章也已经写的非常好了.但是很多同学反应还有一些部分没有讲解的十分清楚,还是有一定的疑惑没有想的十分清楚.因此本文主要结合常见的一些疑问.ThreadLocal源码.应用实例以注意事项来全面而深入地再详细讲解一遍ThreadLoca

  • 设计模式系列之组合模式及其在JDK和MyBatis源码中的运用详解

    组合模式及其在JDK源码中的运用 前言组合和聚合什么是组合模式示例透明组合模式透明组合模式的缺陷安全组合模式 组合模式角色组合模式在JDK源码中的体现组合模式应用场景享元模式优缺点总结 前言 本文主要会讲述组合模式的用法,并会结合在JDK和MyBatis源码中的运用来进一步理解组合模式. 在编码原则中,有一条是:多用组合,少用继承.当然这里的组合和我们今天要讲的组合模式并不等价,这里的组合其实就是一种聚合,那么聚合和组合有什么区别呢? 组合和聚合 人在一起叫团伙,心在一起叫团队.用这句话来诠释组

  • JDK源码分析之String、StringBuilder和StringBuffer

    前言 本文主要介绍了关于JDK源码分析之String.StringBuilder和StringBuffer的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧 String类的申明 public final class String implements java.io.Serializable, Comparable<String>, CharSequence {-} String类用了final修饰符,表示它不可以被继承,同时还实现了三个接口, 实现Serializa

随机推荐