Java中的增强for循环的实现原理与坑

在JAVA中,遍历集合和数组一般有以下三种形式:

for (int i = 0; i < list.size(); i++) {
   System.out.print(list.get(i) + ",");
}

Iterator iterator = list.iterator();
while (iterator.hasNext()) {
   System.out.print(iterator.next() + ",");
}

for (Integer i : list) {
   System.out.print(i + ",");
}

*种是普通的for循环遍历、第二种是使用迭代器进行遍历,第三种我们一般称之为增强for循环(for each)。

实现原理

可以看到,第三种形式是JAVA提供的语法糖,这里我们剖析一下,这种增强for循环底层是如何实现的。

我们对以下代码进行反编译

for (Integer : list) {
   System.out.println(i);
}

反编译后:

Integer i;
for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(i)){
   i = (Integer)iterator.next();        
}

反编译后的代码其实比较复杂,我们按照执行顺序拆解一下:

Integer i; 定义一个临时变量i

Iterator iterator = list.iterator(); 获取List的迭代器

iterator.hasNext(); 判断迭代器中是否有未遍历过的元素

i = (Integer)iterator.next(); 获取*个未遍历的元素,赋值给临时变量i

System.out.println(i) 输出临时变量i的值

如此循环往复,直到遍历完List中的所有元素。

通过反编译,我们看到,其实JAVA中的增强for循环底层是通过迭代器模式来实现的。

增强for循环的坑

这里说是增强for循环的坑,其实主要是因为有些人不了解增强for循环的实现原理而可能踩入的坑。

既然增强for循环通过迭代器实现,那么必然有迭代器的特性。

Java中有fail-fast机制。在使用迭代器遍历元素的时候,在对集合进行删除的时候一定要注意,使用不当有可能发生ConcurrentModificationException,这是一种运行时异常,编译期并不会发生。只有在程序真正运行时才会爆发。

如以下代码:

for (Student stu : students) {    
   if (stu.getId() == 2)     
       students.remove(stu);    
}

会抛出ConcurrentModificationException异常。

Iterator是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出

java.util.ConcurrentModificationException异常。

所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。

但你可以使用 Iterator 本身的方法 remove() 来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。

正确的在遍历的同时删除元素的姿势:

Iterator<Student> stuIter = students.iterator();    
while (stuIter.hasNext()) {    
   Student student = stuIter.next();    
   if (student.getId() == 2)    
       stuIter.remove();//这里要使用Iterator的remove方法移除当前对象,如果使用List的remove方法,则同样会出现ConcurrentModificationException    
}

好啦,这里给你介绍了增强for循环的实现原理,以及使用不当可能踩入的坑。所以,虽然是一个简单的for-each语法,但是也要了解其原理,不然可能导致一些莫名其妙的问题。

转载自:http://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650121134&idx=1&sn=a34a1bd547f00e479e9f6dbde8848fe4&chksm=f36bbe8fc41c3799d1bb2c781f81f51e28651d2fb8eb5670a31caac5ba782b66416e5fdf1b1c&mpshare=1&scene=1&srcid=0413o4rBaecPGUVy6Bi9lzt7#rd

js中let和var的区别

转载自:https://www.cnblogs.com/asand/p/7205632.html

let变量之前没见过,刚遇到,探探究竟。

以下转自:http://blog.csdn.net/nfer_zhuang/article/details/48781671

声明后未赋值,表现相同

复制代码

复制代码

(function() {
      var varTest;
      let letTest;
      console.log(varTest); //输出undefined
      console.log(letTest); //输出undefined
    }());

复制代码

复制代码

使用未声明的变量,表现不同:

复制代码

复制代码

(function() {
  console.log(varTest); //输出undefined(注意要注释掉下面一行才能运行)
  console.log(letTest); //直接报错:ReferenceError: letTest is not defined

  var varTest = 'test var OK.';
  let letTest = 'test let OK.';
}());

复制代码

复制代码

重复声明同一个变量时,表现不同:

复制代码

复制代码

(function() {
      "use strict";
      var varTest = 'test var OK.';
      let letTest = 'test let OK.';

      var varTest = 'varTest changed.';
      let letTest = 'letTest changed.'; //直接报错:SyntaxError: Identifier 'letTest' has already been declared

      console.log(varTest); //输出varTest changed.(注意要注释掉上面letTest变量的重复声明才能运行)
      console.log(letTest);
    }());

复制代码

复制代码

变量作用范围,表现不同:

复制代码

复制代码

(function() {
  var varTest = 'test var OK.';
  let letTest = 'test let OK.';

  {
    var varTest = 'varTest changed.';
    let letTest = 'letTest changed.';
  }

  console.log(varTest); //输出"varTest changed.",内部"{}"中声明的varTest变量覆盖外部的letTest声明
  console.log(letTest); //输出"test let OK.",内部"{}"中声明的letTest和外部的letTest不是同一个变量
}());

复制代码

复制代码

备注:

使用 let 语句声明一个变量,该变量的范围限于声明它的块中。  可以在声明变量时为变量赋值,也可以稍后在脚本中给变量赋值。

使用 let 声明的变量,在声明前无法使用,否则将会导致错误。

如果未在 let 语句中初始化您的变量,则将自动为其分配 JavaScript 值 undefined

activemq持久订阅工作原理

对activemq消息订阅模式来说有两种:持久订阅/非持久订阅。

非持久订阅consumer只能消费在该consumer激活状态时传送给对应topic的消息才能被该consumer消费,一旦该consumer 挂掉到下次启动期间发布到该topic的消息不能被该consumer重新恢复时使用!!!

持久订阅:订阅之后,无论消息是否是在该consumer激活或者down掉期间发送的,*终都会被该consumer接收到,直到被显示取消持久订阅(session.unscribe(“topic名字”))!!!

 

那么持久订阅到底是如何实现的呢,笔者在这里将展现其中的奥秘:

先来看下TopicRegion的addConsumer方法

public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
if (info.isDurable()) {
//看该消息是否是持久化订阅

ActiveMQDestination destination = info.getDestination();
if (!destination.isPattern()) {
// Make sure the destination is created.
lookup(context, destination,true);
}
String clientId = context.getClientId();
String subscriptionName = info.getSubscriptionName();
SubscriptionKey key = new SubscriptionKey(clientId, subscriptionName);
DurableTopicSubscription sub = durableSubscriptions.get(key);
if (sub != null) {
if (sub.isActive()) {
throw new JMSException(“Durable consumer is in use for client: ” + clientId + ” and subscriptionName: ” + subscriptionName);
}
// 看下该订阅者的消息筛选项是否变化
if (hasDurableSubChanged(info, sub.getConsumerInfo())) {
// 如果变化了那么首先移除该订阅者对应的DurableTopicSubscription,然后再追加*新创建的DurableTopicSubscription
durableSubscriptions.remove(key);
destinationsLock.readLock().lock();
try {
for (Destination dest : destinations.values()) {
//Account for virtual destinations
if (dest instanceof Topic){
Topic topic = (Topic)dest;
topic.deleteSubscription(context, key);
}
}
} finally {
destinationsLock.readLock().unlock();
}
super.removeConsumer(context, sub.getConsumerInfo());
super.addConsumer(context, info);
sub = durableSubscriptions.get(key);
} else {
// 如果消息筛选项没有变化,那么直接将刚恢复连接的订阅者id与之前的DurableTopicSubscription 关联起来
if (sub.getConsumerInfo().getConsumerId() != null) {
subscriptions.remove(sub.getConsumerInfo().getConsumerId());
}
subscriptions.put(info.getConsumerId(), sub);
}
} else {
super.addConsumer(context, info);
sub = durableSubscriptions.get(key);
if (sub == null) {
throw new JMSException(“Cannot use the same consumerId: ” + info.getConsumerId() + ” for two different durable subscriptions clientID: ” + key.getClientId()
+ ” subscriberName: ” + key.getSubscriptionName());
}
}
sub.activate(usageManager, context, info, broker);
return sub;
} else {
return super.addConsumer(context, info);
}
}

上面代码是订阅者连接到消息提供者时的处理代码,下面看下更核心的持久订阅与消息提供者断开连接时的处理:

@Override
public void removeConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
if (info.isDurable()) {
SubscriptionKey key = new SubscriptionKey(context.getClientId(), info.getSubscriptionName());
DurableTopicSubscription sub = durableSubscriptions.get(key);
if (sub != null) {
sub.deactivate(keepDurableSubsActive);
}

} else {
super.removeConsumer(context, info);
}
}

从上面代码可以看到,针对持久订阅者来说,当其与消息提供者断开连接时,provider并没有将该连接移除,仅仅是将断开连接者对应的DurableTopicSubscription状态设置为非激活状态,改状态不影响provider将发送到该topic的消息保存下来,非持久订阅者则在与provider失去连接这段期间无法接收该时间段发送的消息!

pkcs1与pkcs8格式RSA私钥互相转换

注:亲验可用

转载自:https://www.jianshu.com/p/08e41304edab

1、PKCS1私钥生成

openssl genrsa -out private.pem 1024

private.pem 的内容如下:

  1. —–BEGIN RSA PRIVATE KEY—–
  2. MIICXAIBAAKBgQC5BW6T9GVaaG/epGDjPpY3wN0DrBt+NojvxkEgpUdOAxgAepqe
  3. GbSqtXAd+MOOBbHxIOEwrFC9stkypQgxrB49tXDI+4Jj8MuKI15HEmI8k7+tRDOl
  4. J5TFSL2J9KA3GuQbyVAhlpxl+YnV7yjxP9l1dkbApg1ixSd5KOPbaQ00WQIDAQAB
  5. AoGAYiqzpOTC8dj/og1tKqUGZsZ5fX1PiQO+XBnAbGXFE2sozPhAGSpiZUCnH//h
  6. IfV7mAht8rk6java+bf+RPyhfg0zW7oXy0pm8DwoW7+0fOzQ4sEYeoqza/VrkYwR
  7. 5BxBa+KyT1HCi4uXogyDlQT1p0ZT0iaqZBfTApdyVkmcQEECQQDhfPl+ILl0bh0H
  8. 8ORoMmmxAZMn293+de441OlAjL3CsF4yhUUdavAYWM0RAV5MJtKUTR4ZpRXkB/pq
  9. kgyTxpr9AkEA0g6pQRpcGxulr2758ZlOLdL8B1n1ubre464IKQ0zNfERKhR/j7U8
  10. LGF+3mhZuoSEdklwLCJ8ZMvIhkV0v8JjjQJBANtqXOyas1vUenNruRabV7ViLuuu
  11. S0p9Px4WMBMb4Ns9+6t1e1ew44kNgB54EmZPsMGWeR/DQJXwHYDuNUbnD5ECQA7S
  12. Gf8N7RG8kaQfIGN7fZieGkoqfrvsA23tCYZb+BEGQT/G0nlBQE2hU2I92pbeYro1
  13. 1ERI6p3yAuP2YpZlEMECQGNzhqshYfDiWwU4Q3aZWkRrv74uIXk1HQoFH1BthzQJ
  14. TbzKH/LEqZN8WVau3bf41yAx2YoaOsIJJtOUTYcfh14=
  15. —–END RSA PRIVATE KEY—–

2、PKCS1私钥转换为PKCS8(该格式一般Java调用)

  1. openssl pkcs8 -topk8 -inform PEM -in private.pem -outform pem -nocrypt -out pkcs8.pem

pkcs8.pem文件内容

  1. —–BEGIN PRIVATE KEY—–
  2. MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALkFbpP0ZVpob96k
  3. YOM+ljfA3QOsG342iO/GQSClR04DGAB6mp4ZtKq1cB34w44FsfEg4TCsUL2y2TKl
  4. CDGsHj21cMj7gmPwy4ojXkcSYjyTv61EM6UnlMVIvYn0oDca5BvJUCGWnGX5idXv
  5. KPE/2XV2RsCmDWLFJ3ko49tpDTRZAgMBAAECgYBiKrOk5MLx2P+iDW0qpQZmxnl9
  6. fU+JA75cGcBsZcUTayjM+EAZKmJlQKcf/+Eh9XuYCG3yuTqNq9r5t/5E/KF+DTNb
  7. uhfLSmbwPChbv7R87NDiwRh6irNr9WuRjBHkHEFr4rJPUcKLi5eiDIOVBPWnRlPS
  8. JqpkF9MCl3JWSZxAQQJBAOF8+X4guXRuHQfw5GgyabEBkyfb3f517jjU6UCMvcKw
  9. XjKFRR1q8BhYzREBXkwm0pRNHhmlFeQH+mqSDJPGmv0CQQDSDqlBGlwbG6Wvbvnx
  10. mU4t0vwHWfW5ut7jrggpDTM18REqFH+PtTwsYX7eaFm6hIR2SXAsInxky8iGRXS/
  11. wmONAkEA22pc7JqzW9R6c2u5FptXtWIu665LSn0/HhYwExvg2z37q3V7V7DjiQ2A
  12. HngSZk+wwZZ5H8NAlfAdgO41RucPkQJADtIZ/w3tEbyRpB8gY3t9mJ4aSip+u+wD
  13. be0Jhlv4EQZBP8bSeUFATaFTYj3alt5iujXUREjqnfIC4/ZilmUQwQJAY3OGqyFh
  14. 8OJbBThDdplaRGu/vi4heTUdCgUfUG2HNAlNvMof8sSpk3xZVq7dt/jXIDHZiho6
  15. wgkm05RNhx+HXg==
  16. —–END PRIVATE KEY—–

3、PKCS8格式私钥转换为PKCS1(传统私钥格式)

openssl rsa -in pkcs8.pem -out pkcs1.pem

pkcs1.pem文件内容如下:

  1. —–BEGIN RSA PRIVATE KEY—–
  2. MIICXAIBAAKBgQC5BW6T9GVaaG/epGDjPpY3wN0DrBt+NojvxkEgpUdOAxgAepqe
  3. GbSqtXAd+MOOBbHxIOEwrFC9stkypQgxrB49tXDI+4Jj8MuKI15HEmI8k7+tRDOl
  4. J5TFSL2J9KA3GuQbyVAhlpxl+YnV7yjxP9l1dkbApg1ixSd5KOPbaQ00WQIDAQAB
  5. AoGAYiqzpOTC8dj/og1tKqUGZsZ5fX1PiQO+XBnAbGXFE2sozPhAGSpiZUCnH//h
  6. IfV7mAht8rk6java+bf+RPyhfg0zW7oXy0pm8DwoW7+0fOzQ4sEYeoqza/VrkYwR
  7. 5BxBa+KyT1HCi4uXogyDlQT1p0ZT0iaqZBfTApdyVkmcQEECQQDhfPl+ILl0bh0H
  8. 8ORoMmmxAZMn293+de441OlAjL3CsF4yhUUdavAYWM0RAV5MJtKUTR4ZpRXkB/pq
  9. kgyTxpr9AkEA0g6pQRpcGxulr2758ZlOLdL8B1n1ubre464IKQ0zNfERKhR/j7U8
  10. LGF+3mhZuoSEdklwLCJ8ZMvIhkV0v8JjjQJBANtqXOyas1vUenNruRabV7ViLuuu
  11. S0p9Px4WMBMb4Ns9+6t1e1ew44kNgB54EmZPsMGWeR/DQJXwHYDuNUbnD5ECQA7S
  12. Gf8N7RG8kaQfIGN7fZieGkoqfrvsA23tCYZb+BEGQT/G0nlBQE2hU2I92pbeYro1
  13. 1ERI6p3yAuP2YpZlEMECQGNzhqshYfDiWwU4Q3aZWkRrv74uIXk1HQoFH1BthzQJ
  14. TbzKH/LEqZN8WVau3bf41yAx2YoaOsIJJtOUTYcfh14=
  15. —–END RSA PRIVATE KEY—–

eclipse中调试时无法进入jdk源码

转载自:https://blog.csdn.net/u013352983/article/details/50633637

亲试有效

今天在eclipse中进行断点调试时,无法进入jdk源码中跟踪代码,eclipse默认是使用的java环境是JRE(Java Runtime Environment 即java运行时环境)环境,jre环境是不支持调试的;需要将eclipse的环境换成JDK的。查看eclipse运行环境方法如下: window  –>  preference  –>  java  –>  Installed JREs –> 右侧 会看到 eclipse的java环境了。

既然JRE不允许调试那就将eclipse的环境换成jdk的配置如下:

*步:找到eclipse环境如下图:

%title插图%num%title插图%num

 

 

第二步:点击add找到本地安装jdk的目录

点击add按钮之后如图:

%title插图%num%title插图%num

点击Next:如图:

%title插图%num%title插图%num

点击Directory….选择jdk的安装文件(是jdk不是jre)选择完成后点击Finish,设置jdk为默认环境如图:

%title插图%num

第三步:将要调试的工程的引用的环境换成jdk的(即  Buil Path)如图:

%title插图%num

现在可以进行jdk源码的调试和跟踪了

NIO 之 ByteBuffer实现原理

转载自:https://www.jianshu.com/p/451cc865d413

前言

Java NIO 主要由下面3部分组成:

  • Buffer
  • Channel
  • Selector

在传统IO中,流是基于字节的方式进行读写的。
在NIO中,使用通道(Channel)基于缓冲区数据块的读写。

流是基于字节一个一个的读取和写入。
通道是基于块的方式进行读取和写入。

Buffer 类结构图

Buffer 的类结构图如下:

 

%title插图%num

Buffer类结构图

从图中发现java中8中基本的类型,除了boolean外,其它的都有特定的Buffer子类。

Buffer类分析

Filed

每个缓冲区都有这4个属性,无论缓冲区是何种类型都有相同的方法来设置这些值

  1. private int mark = -1;
  2. private int position = 0;
  3. private int limit;
  4. private int capacity;

1. 标记(mark)

初始值-1,表示未标记。
标记一个位置,方便以后reset重新从该位置读取数据。

  1. public final Buffer mark() {
  2. mark = position;
  3. return this;
  4. }
  5. public final Buffer reset() {
  6. int m = mark;
  7. if (m < 0)
  8. throw new InvalidMarkException();
  9. position = m;
  10. return this;
  11. }

2. 位置(position)

缓冲区中读取或写入的下一个位置。这个位置从0开始,*大值等于缓冲区的大小

  1. //获取缓冲区的位置
  2. public final int position() {
  3. return position;
  4. }
  5. //设置缓冲区的位置
  6. public final Buffer position(int newPosition) {
  7. if ((newPosition > limit) || (newPosition < 0))
  8. throw new IllegalArgumentException();
  9. position = newPosition;
  10. if (mark > position) mark = -1;
  11. return this;
  12. }

3. 限度(limit)

  1. //获取limit位置
  2. public final int limit() {
  3. return limit;
  4. }
  5. //设置limit位置
  6. public final Buffer limit(int newLimit) {
  7. if ((newLimit > capacity) || (newLimit < 0))
  8. throw new IllegalArgumentException();
  9. limit = newLimit;
  10. if (position > limit) position = limit;
  11. if (mark > limit) mark = -1;
  12. return this;
  13. }

4. 容量(capacity)

缓冲区可以保存元素的*大数量。该值在创建缓存区时指定,一旦创建完成后就不能修改该值。

  1. //获取缓冲区的容量
  2. public final int capacity() {
  3. return capacity;
  4. }

filp 方法

  1. public final Buffer flip() {
  2. limit = position;
  3. position = 0;
  4. mark = –1;
  5. return this;
  6. }
  1. 将limit设置成当前position的坐标
  2. 将position设置为0
  3. 取消标记

rewind 方法

  1. public final Buffer rewind() {
  2. position = 0;
  3. mark = –1;
  4. return this;
  5. }

从源码中发现,rewind修改了position和mark,而没有修改limit。

  1. 将position设置为0
  2. 取消mark标记

clear 方法

  1. public final Buffer clear() {
  2. position = 0;
  3. limit = capacity;
  4. mark = –1;
  5. return this;
  6. }
  1. 将position坐标设置为0
  2. limit设置为capacity
  3. 取消标记

从clear方法中,我们发现Buffer中的数据没有清空,如果通过Buffer.get(i)的方式还是可以访问到数据的。如果再次向缓冲区中写入数据,他会覆盖之前存在的数据。

remaining 方法

查看当前位置和limit之间的元素数。

  1. public final int remaining() {
  2. return limit – position;
  3. }

hasRemaining 方法

判断当前位置和limit之间是否还有元素

  1. public final boolean hasRemaining() {
  2. return position < limit;
  3. }

ByteBuffer 类分析

%title插图%num

ByteBuffer类结果图

从图中我们可以发现 ByteBuffer继承于Buffer类,ByteBuffer是个抽象类,它有两个实现的子类HeapByteBuffer和MappedByteBuffer类

HeapByteBuffer:在堆中创建的缓冲区。就是在jvm中创建的缓冲区。
MappedByteBuffer:直接缓冲区。物理内存中创建缓冲区,而不在堆中创建。

allocate 方法(创建堆缓冲区)

  1. public static ByteBuffer allocate(int capacity) {
  2. if (capacity < 0)
  3. throw new IllegalArgumentException();
  4. return new HeapByteBuffer(capacity, capacity);
  5. }

我们发现allocate方法创建的缓冲区是创建的HeapByteBuffer实例。

HeapByteBuffer 构造

  1. HeapByteBuffer(int cap, int lim) { // package-private
  2. super(-1, 0, lim, cap, new byte[cap], 0);
  3. }

从堆缓冲区中看出,所谓堆缓冲区就是在堆内存中创建一个byte[]数组。

allocateDirect创建直接缓冲区

  1. public static ByteBuffer allocateDirect(int capacity) {
  2. return new DirectByteBuffer(capacity);
  3. }

我们发现allocate方法创建的缓冲区是创建的DirectByteBuffer实例。

DirectByteBuffer构造

%title插图%num

DirectByteBuffer 构造方法

 

直接缓冲区是通过java中Unsafe类进行在物理内存中创建缓冲区。

wrap 方法

  1. public static ByteBuffer wrap(byte[] array)
  2. public static ByteBuffer wrap(byte[] array, int offset, int length);

可以通过wrap类把字节数组包装成缓冲区ByteBuffer实例。
这里需要注意的的,把array的引用赋值给ByteBuffer对象中字节数组。如果array数组中的值更改,则ByteBuffer中的数据也会更改的。

get 方法

  1. public byte get()
    获取position坐标元素,并将position+1;
  2. public byte get(int i)
    获取指定索引下标的元素
  3. public ByteBuffer get(byte[] dst)
    从当前position中读取元素填充到dst数组中,每填充一个元素position+1;
  4. public ByteBuffer get(byte[] dst, int offset, int length)
    从当前position中读取元素到dst数组的offset下标开始填充length个元素。

put 方法

  1. public ByteBuffer put(byte x)
    写入一个元素并position+1
  2. public ByteBuffer put(int i, byte x)
    指定的索引写入一个元素
  3. public final ByteBuffer put(byte[] src)
    写入一个自己数组,并position+数组长度
  4. public ByteBuffer put(byte[] src, int offset, int length)
    从一个自己数组的offset开始length个元素写入到ByteBuffer中,并把position+length
  5. public ByteBuffer put(ByteBuffer src)
    写入一个ByteBuffer,并position加入写入的元素个数

视图缓冲区

%title插图%num

Paste_Image.png

 

ByteBuffer可以转换成其它类型的Buffer。例如CharBuffer、IntBuffer 等。

压缩缓冲区

  1. public ByteBuffer compact() {
  2. System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
  3. position(remaining());
  4. limit(capacity());
  5. discardMark();
  6. return this;
  7. }

1、把缓冲区positoin到limit中的元素向前移动positoin位
2、设置position为remaining()
3、 limit为缓冲区容量
4、取消标记

例如:ByteBuffer.allowcate(10);
内容:[0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9]

compact前

[0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9]
pos=4
lim=10
cap=10

compact后

[4, 5, 6, 7, 8, 9, 6, 7, 8, 9]
pos=6
lim=10
cap=10

slice方法

  1. public ByteBuffer slice() {
  2. return new HeapByteBuffer(hb,
  3. 1,
  4. 0,
  5. this.remaining(),
  6. this.remaining(),
  7. this.position() + offset);
  8. }

创建一个分片缓冲区。分配缓冲区与主缓冲区共享数据。
分配的起始位置是主缓冲区的position位置
容量为limit-position。
分片缓冲区无法看到主缓冲区positoin之前的元素。

直接缓冲区和堆缓冲区性能对比

下面我们从缓冲区创建的性能和读取性能两个方面进行性能对比。

读写性能对比

  1. public static void directReadWrite() throws Exception {
  2. int time = 10000000;
  3. long start = System.currentTimeMillis();
  4. ByteBuffer buffer = ByteBuffer.allocate(4*time);
  5. for(int i=0;i<time;i++){
  6. buffer.putInt(i);
  7. }
  8. buffer.flip();
  9. for(int i=0;i<time;i++){
  10. buffer.getInt();
  11. }
  12. System.out.println(“堆缓冲区读写耗时 :”+(System.currentTimeMillis()-start));
  13. start = System.currentTimeMillis();
  14. ByteBuffer buffer2 = ByteBuffer.allocateDirect(4*time);
  15. for(int i=0;i<time;i++){
  16. buffer2.putInt(i);
  17. }
  18. buffer2.flip();
  19. for(int i=0;i<time;i++){
  20. buffer2.getInt();
  21. }
  22. System.out.println(“直接缓冲区读写耗时:”+(System.currentTimeMillis()-start));
  23. }

输出结果:

  1. 堆缓冲区创建耗时 :70
  2. 直接缓冲区创建耗时:47

从结果中我们发现堆缓冲区读写比直接缓冲区读写耗时更长。

  1. public static void directAllocate() throws Exception {
  2. int time = 10000000;
  3. long start = System.currentTimeMillis();
  4. for (int i = 0; i < time; i++) {
  5. ByteBuffer buffer = ByteBuffer.allocate(4);
  6. }
  7. System.out.println(“堆缓冲区创建时间:”+(System.currentTimeMillis()-start));
  8. start = System.currentTimeMillis();
  9. for (int i = 0; i < time; i++) {
  10. ByteBuffer buffer = ByteBuffer.allocateDirect(4);
  11. }
  12. System.out.println(“直接缓冲区创建时间:”+(System.currentTimeMillis()-start));
  13. }

输出结果:

  1. 堆缓冲区创建时间:73
  2. 直接缓冲区创建时间:5146

从结果中发现直接缓冲区创建分配空间比较耗时。

对比结论

直接缓冲区比较适合读写操作,*好能重复使用直接缓冲区并多次读写的操作。
堆缓冲区比较适合创建新的缓冲区,并且重复读写不会太多的应用。

建议:如果经过性能测试,发现直接缓冲区确实比堆缓冲区效率高才使用直接缓冲区,否则不建议使用直接缓冲区。

NIO 之 Buffer 图解

转载自:https://www.jianshu.com/p/12c81abb5387

 

Buffer 类 结构

%title插图%num

对于每个非布尔原始数据类型都有一个缓冲区类。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。

概述

缓冲区 Buffer 内部就是用数组实现的。 Buffer 包含了下面4个属性:

  • 容量( Capacity)
    缓冲区能够容纳的数据元素的*大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
  • 上界( Limit)
    缓冲区的*个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
  • 位置( Position)
    下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
  • 标记( Mark)
    一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position = mark。标记在设定前是未定义的(undefined)。

这四个属性之间总是遵循以下关系:
0 <= mark <= position <= limit <= capacity

示例

下面展示了一个新创建的容量为 10 的 ByteBuffer 逻辑视图

ByteBuffer.allocate(10);

%title插图%num

图1

位置(Position)被设为 0,而且容量( Capacity)和上界( Limit)被设为 10,刚好经过缓冲区能够容纳的*后一个字节。
标记(mark)*初未定义。
容量(Capacity)是固定的,但另外的三个属性可以在使用缓冲区时改变。

put() 方法

让我们看一个例子。 我们将代表“abcde”字符串的 ASCII 码载入一个名为 buffer 的
ByteBuffer 对象中。当在图1 中所新建的缓冲区上执行以下代码后。

buffer.put((byte)'a').put((byte)'b').put((byte)'c').put((byte)'d').put((byte)'e');

缓冲区的结果状态如图 2所示:

 

%title插图%num

图2

flip() 方法

我们已经写满了缓冲区,现在我们必须准备将其清空。我们想把这个缓冲区传递给一个通
道,以使内容能被全部写出。但如果通道现在在缓冲区上执行 get(),那么它将从我们刚刚插入的有用数据之外取出未定义数据。如果我们将位置值重新设为 0,通道就会从正确位置开始获取,但是它是怎样知道何时到达我们所插入数据末端的呢?这就是上界属性被引入的目的。上界属性指明了缓冲区有效内容的末端。我们需要将上界属性设置为当前位置,然后将位置重置为 0。

flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素
的释放状态。在翻转之后,图 2 的缓冲区会变成图 3 中的样子。

%title插图%num

图3

rewind() 方法

rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。您可以使
用 rewind()后退,重读已经被翻转的缓冲区中的数据。
图2 的缓冲区调用 rewind() 方法会变成图4 中的样子。

 

%title插图%num

图4

如果将缓冲区翻转两次会怎样呢?

compact() 方法

有时,您可能只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。为了实现这
一点,未读的数据元素需要下移以使*个元素索引为 0。尽管重复这样做会效率低下,但这有时非常必要,而 API 对此为您提供了一个 compact()函数。这一缓冲区工具在复制数据时要比您使用 get()和 put()函数高效得多。所以当您需要时,请使用 compact()。图 5显示了一个读取了两个元素(position 现在为2),并且现在我们想要对其进行压缩的缓冲区。

%title插图%num

图5

buffer.compact();

压缩后的结果如下图

 

%title插图%num

图6

duplicate() 方法

duplicate() 方法创建了一个与原始缓冲区一样的新缓冲区。两个缓冲区共享数据,拥有同样的 capacity ,但每个缓冲区都拥有自己的 position,limit 和 mark 属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。

  1. public ByteBuffer duplicate() {
  2. return new HeapByteBufferR(hb,
  3. this.markValue(),
  4. this.position(),
  5. this.limit(),
  6. this.capacity(),
  7. offset);
  8. }

重新创建一个 ByteBuffer,并且使用同一个数组。所有一个byteBuffer 变动,会影响另一个 ByteBuffer。 但 position、limit、mark 都是独立的。

duplicate() 方法

您 可 以 使 用 asReadOnlyBuffer() 函 数 来 生 成 一 个 只 读 的 缓 冲 区 视 图 。 这 与
duplicate()相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly()函数
将 会 返 回 true 。 对 这 一 只 读 缓 冲 区 的 put() 函 数 的 调 用 尝 试 会 导 致 抛 出
ReadOnlyBufferException 异常。

  1. public ByteBuffer asReadOnlyBuffer() {
  2. return new HeapByteBufferR(hb,
  3. this.markValue(),
  4. this.position(),
  5. this.limit(),
  6. this.capacity(),
  7. offset);
  8. }

HeapByteBufferR 分析

  1. class HeapByteBufferR
  2. extends HeapByteBuffer{
  3. public ByteBuffer put(byte x) {
  4. throw new ReadOnlyBufferException();
  5. }
  6. public ByteBuffer put(int i, byte x) {
  7. throw new ReadOnlyBufferException();
  8. }
  9. public ByteBuffer putInt(int x) {
  10. throw new ReadOnlyBufferException();
  11. }
  12. ……
  13. }

HeapByteBufferR 继承 HeapByteBuffer 类,并重写了所有的可修改 buffer 的方法。把所有能修改 buffer 的方法都直接 throw ReadOnlyBufferException,来保证只读。

slice() 方法

slice() 分割缓冲区。创建一个从原始缓冲区的当前位置开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数量( limit-position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。

原 ByteBuffer如下图:

 

%title插图%num

slice() 分割后的 ByteBuffer

 

%title插图%num

  1. public ByteBuffer slice() {
  2. return new HeapByteBuffer(hb,
  3. 1,
  4. 0,
  5. this.remaining(),
  6. this.remaining(),
  7. this.position() + offset);
  8. }

 

Java 8 中的 Streams API 详解

转载自:https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/index.html

为什么需要 Stream

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,*大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

什么是聚合操作

在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的聚合操作来完成诸如:

  • 客户每月平均消费金额
  • *昂贵的在售商品
  • 本周完成的有效订单(排除了无效的)
  • 取十个数据样本作为首页推荐

这类的操作。

但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。而 Java 的集合 API 中,仅仅有*少量的辅助型方法,更多的时候是程序员需要用 Iterator 来遍历集合,完成相关的聚合应用逻辑。这是一种远不够高效、笨拙的方法。在 Java 7 中,如果要发现 type 为 grocery 的所有交易,然后返回以交易值降序排序好的交易 ID 集合,我们需要这样写:

清单 1. Java 7 的排序、取值实现

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

List<Transaction> groceryTransactions = new Arraylist<>();

for(Transaction t: transactions){

 if(t.getType() == Transaction.GROCERY){

 groceryTransactions.add(t);

 }

}

Collections.sort(groceryTransactions, new Comparator(){

 public int compare(Transaction t1, Transaction t2){

 return t2.getValue().compareTo(t1.getValue());

 }

});

List<Integer> transactionIds = new ArrayList<>();

for(Transaction t: groceryTransactions){

 transactionsIds.add(t.getId());

}

而在 Java 8 使用 Stream,代码更加简洁易读;而且使用并发模式,程序执行速度更快。

清单 2. Java 8 的排序、取值实现

1

2

3

4

5

List<Integer> transactionsIds = transactions.parallelStream().

 filter(t -> t.getType() == Transaction.GROCERY).

 sorted(comparing(Transaction::getValue).reversed()).

 map(Transaction::getId).

 collect(toList());

Stream 总览

什么是流

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。Java 的并行 API 演变历程基本如下:

  1. 1.0-1.4 中的 java.lang.Thread
  2. 5.0 中的 java.util.concurrent
  3. 6.0 中的 Phasers 等
  4. 7.0 中的 Fork/Join 框架
  5. 8.0 中的 Lambda

Stream 的另外一大特点是,数据源本身可以是无限的。

流的构成

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。

图 1. 流管道 (Stream Pipeline) 的构成

图 1.  流管道 (Stream Pipeline) 的构成

有多种方式生成 Stream Source:

  • 从 Collection 和数组
    • Collection.stream()
    • Collection.parallelStream()
    • Arrays.stream(T array) or Stream.of()

    从 BufferedReader

    • java.io.BufferedReader.lines()
  • 静态工厂
  • java.util.stream.IntStream.range()
  • java.nio.file.Files.walk()
  • 自己构建
    • java.util.Spliterator

    其它

    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()

流的操作类型分为两种:

  • Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的*后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

还有一种操作被称为 short-circuiting。用以指:

  • 对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
  • 对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

清单 3. 一个流操作的示例

1

2

3

4

int sum = widgets.stream()

.filter(w -> w.getColor() == RED)

 .mapToInt(w -> w.getWeight())

 .sum();

stream() 获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,*后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。

流的使用详解

简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个*终结果,或者导致一个副作用(side effect)。

流的构造与转换

下面提供*常见的几种构造 Stream 的样例。

清单 4. 构造流的几种常见方法

1

2

3

4

5

6

7

8

9

// 1. Individual values

Stream stream = Stream.of("a", "b", "c");

// 2. Arrays

String [] strArray = new String[] {"a", "b", "c"};

stream = Stream.of(strArray);

stream = Arrays.stream(strArray);

// 3. Collections

List<String> list = Arrays.asList(strArray);

stream = list.stream();

需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:

IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。

Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。

清单 5. 数值流的构造

1

2

3

IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);

IntStream.range(1, 3).forEach(System.out::println);

IntStream.rangeClosed(1, 3).forEach(System.out::println);

清单 6. 流转换为其它数据结构

1

2

3

4

5

6

7

8

9

// 1. Array

String[] strArray1 = stream.toArray(String[]::new);

// 2. Collection

List<String> list1 = stream.collect(Collectors.toList());

List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));

Set set1 = stream.collect(Collectors.toSet());

Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));

// 3. String

String str = stream.collect(Collectors.joining()).toString();

一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。

流的操作

接下来,当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。

  • Intermediate:

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  • Terminal:

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  • Short-circuiting:

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

我们下面看一下 Stream 的比较典型用法。

map/flatMap

我们先来看 map。如果你熟悉 scala 这类函数式语言,对这个方法应该很了解,它的作用就是把 input Stream 的每一个元素,映射成 output Stream 的另外一个元素。

清单 7. 转换大写

1

2

3

List<String> output = wordList.stream().

map(String::toUpperCase).

collect(Collectors.toList());

这段代码把所有的单词转换为大写。

清单 8. 平方数

1

2

3

4

List<Integer> nums = Arrays.asList(1, 2, 3, 4);

List<Integer> squareNums = nums.stream().

map(n -> n * n).

collect(Collectors.toList());

这段代码生成一个整数 list 的平方数 {1, 4, 9, 16}。

从上面例子可以看出,map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。

清单 9. 一对多

1

2

3

4

5

6

7

Stream<List<Integer>> inputStream = Stream.of(

 Arrays.asList(1),

 Arrays.asList(2, 3),

 Arrays.asList(4, 5, 6)

 );

Stream<Integer> outputStream = inputStream.

flatMap((childList) -> childList.stream());

flatMap 把 input Stream 中的层级结构扁平化,就是将*底层元素抽出来放到一起,*终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。

filter

filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。

清单 10. 留下偶数

1

2

3

Integer[] sixNums = {1, 2, 3, 4, 5, 6};

Integer[] evens =

Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

经过条件“被 2 整除”的 filter,剩下的数字为 {2, 4, 6}。

清单 11. 把单词挑出来

1

2

3

4

List<String> output = reader.lines().

 flatMap(line -> Stream.of(line.split(REGEXP))).

 filter(word -> word.length() > 0).

 collect(Collectors.toList());

这段代码首先把每行的单词用 flatMap 整理到新的 Stream,然后保留长度不为 0 的,就是整篇文章中的全部单词了。

forEach

forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。

清单 12. 打印姓名(forEach 和 pre-java8 的对比)

1

2

3

4

5

6

7

8

9

10

// Java 8

roster.stream()

 .filter(p -> p.getGender() == Person.Sex.MALE)

 .forEach(p -> System.out.println(p.getName()));

// Pre-Java 8

for (Person p : roster) {

 if (p.getGender() == Person.Sex.MALE) {

 System.out.println(p.getName());

 }

}

对一个人员集合遍历,找出男性并打印姓名。可以看出来,forEach 是为 Lambda 而设计的,保持了*紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach 本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。

但一般认为,forEach 和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统 Java 风格的差别。

另外一点需要注意,forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码是错误的:

1

2

stream.forEach(element -> doOneThing(element));

stream.forEach(element -> doAnotherThing(element));

相反,具有相似功能的 intermediate 操作 peek 可以达到上述目的。如下是出现在该 api javadoc 上的一个示例。

清单 13. peek 对每个元素执行操作并返回一个新的 Stream

1

2

3

4

5

6

Stream.of("one", "two", "three", "four")

 .filter(e -> e.length() > 3)

 .peek(e -> System.out.println("Filtered value: " + e))

 .map(String::toUpperCase)

 .peek(e -> System.out.println("Mapped value: " + e))

 .collect(Collectors.toList());

forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。

findFirst

这是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的*个元素,或者空。

这里比较重点的是它的返回值类型:Optional。这也是一个模仿 Scala 语言中的概念,作为一个容器,它可能含有某值,或者不包含。使用它的目的是尽可能避免 NullPointerException。

清单 14. Optional 的两个用例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

String strA = " abcd ", strB = null;

print(strA);

print("");

print(strB);

getLength(strA);

getLength("");

getLength(strB);

public static void print(String text) {

 // Java 8

 Optional.ofNullable(text).ifPresent(System.out::println);

 // Pre-Java 8

 if (text != null) {

 System.out.println(text);

 }

 }

public static int getLength(String text) {

 // Java 8

return Optional.ofNullable(text).map(String::length).orElse(-1);

 // Pre-Java 8

// return if (text != null) ? text.length() : -1;

 };

在更复杂的 if (xx != null) 的情况中,使用 Optional 代码的可读性更好,而且它提供的是编译时检查,能*大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。

Stream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。还有例如 IntStream.average() 返回 OptionalDouble 等等。

reduce

这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的*个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于

Integer sum = integers.reduce(0, (a, b) -> a+b); 或

Integer sum = integers.reduce(0, Integer::sum);

也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。

清单 15. reduce 的用例

1

2

3

4

5

6

7

8

9

10

11

12

// 字符串连接,concat = "ABCD"

String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);

// 求*小值,minValue = -3.0

double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);

// 求和,sumValue = 10, 有起始值

int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);

// 求和,sumValue = 10, 无起始值

sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();

// 过滤,字符串连接,concat = "ace"

concat = Stream.of("a", "B", "c", "D", "e", "F").

 filter(x -> x.compareTo("Z") > 0).

 reduce("", String::concat);

上面代码例如*个示例的 reduce(),*个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator。这类有起始值的 reduce() 都返回具体的对象。而对于第四个示例没有起始值的 reduce(),由于可能没有足够的元素,返回的是 Optional,请留意这个区别。

limit/skip

limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素(它是由一个叫 subStream 的方法改名而来)。

清单 16. limit 和 skip 对运行次数的影响

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

public void testLimitAndSkip() {

 List<Person> persons = new ArrayList();

 for (int i = 1; i <= 10000; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<String> personList2 = persons.stream().

map(Person::getName).limit(10).skip(3).collect(Collectors.toList());

 System.out.println(personList2);

}

private class Person {

 public int no;

 private String name;

 public Person (int no, String name) {

 this.no = no;

 this.name = name;

 }

 public String getName() {

 System.out.println(name);

 return name;

 }

}

输出结果为:

1

2

3

4

5

6

7

8

9

10

11

name1

name2

name3

name4

name5

name6

name7

name8

name9

name10

[name4, name5, name6, name7, name8, name9, name10]

这是一个有 10,000 个元素的 Stream,但在 short-circuiting 操作 limit 和 skip 的作用下,管道中 map 操作指定的 getName() 方法的执行次数为 limit 所限定的 10 次,而*终返回结果在跳过前 3 个元素后只有后面 7 个返回。

有一种情况是 limit/skip 无法达到 short-circuiting 目的的,就是把它们放在 Stream 的排序操作后,原因跟 sorted 这个 intermediate 操作有关:此时系统并不知道 Stream 排序后的次序如何,所以 sorted 中的操作看上去就像完全没有被 limit 或者 skip 一样。

清单 17. limit 和 skip 对 sorted 后的运行次数无影响

1

2

3

4

5

6

7

8

List<Person> persons = new ArrayList();

 for (int i = 1; i <= 5; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<Person> personList2 = persons.stream().sorted((p1, p2) ->

p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList());

System.out.println(personList2);

上面的示例对清单 13 做了微调,首先对 5 个元素的 Stream 排序,然后进行 limit 操作。输出结果为:

1

2

3

4

5

6

7

8

9

name2

name1

name3

name2

name4

name3

name5

name4

[stream.StreamDW$Person@816f27d, stream.StreamDW$Person@87aac27]

即虽然*后的返回元素数量是 2,但整个管道中的 sorted 表达式执行次数没有像前面例子相应减少。

*后有一点需要注意的是,对一个 parallel 的 Steam 管道来说,如果其元素是有序的,那么 limit 操作的成本会比较大,因为它的返回对象必须是前 n 个也有一样次序的元素。取而代之的策略是取消元素间的次序,或者不要用 parallel Stream。

sorted

对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。我们对清单 14 进行优化:

清单 18. 优化:排序前进行 limit 和 skip

1

2

3

4

5

6

7

List<Person> persons = new ArrayList();

 for (int i = 1; i <= 5; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());

System.out.println(personList2);

结果会简单很多:

1

2

3

name2

name1

[stream.StreamDW$Person@6ce253f1, stream.StreamDW$Person@53d8d10a]

当然,这种优化是有 business logic 上的局限性的:即不要求排序后再取值。

min/max/distinct

min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。同时它们作为特殊的 reduce 方法被独立出来也是因为求*大*小值是很常见的操作。

清单 19. 找出*长一行的长度

1

2

3

4

5

6

7

BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));

int longest = br.lines().

 mapToInt(String::length).

 max().

 getAsInt();

br.close();

System.out.println(longest);

下面的例子则使用 distinct 来找出不重复的单词。

清单 20. 找出全文的单词,转小写,并排序

1

2

3

4

5

6

7

8

9

List<String> words = br.lines().

 flatMap(line -> Stream.of(line.split(" "))).

 filter(word -> word.length() > 0).

 map(String::toLowerCase).

 distinct().

 sorted().

 collect(Collectors.toList());

br.close();

System.out.println(words);

Match

Stream 有三个 match 方法,从语义上说:

  • allMatch:Stream 中全部元素符合传入的 predicate,返回 true
  • anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true
  • noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true

它们都不是要遍历全部元素才能返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。对清单 13 中的 Person 类稍做修改,加入一个 age 属性和 getAge 方法。

清单 21. 使用 Match

1

2

3

4

5

6

7

8

9

10

11

12

List<Person> persons = new ArrayList();

persons.add(new Person(1, "name" + 1, 10));

persons.add(new Person(2, "name" + 2, 21));

persons.add(new Person(3, "name" + 3, 34));

persons.add(new Person(4, "name" + 4, 6));

persons.add(new Person(5, "name" + 5, 55));

boolean isAllAdult = persons.stream().

 allMatch(p -> p.getAge() > 18);

System.out.println("All are adult? " + isAllAdult);

boolean isThereAnyChild = persons.stream().

 anyMatch(p -> p.getAge() < 12);

System.out.println("Any child? " + isThereAnyChild);

输出结果:

1

2

All are adult? false

Any child? true

进阶:自己生成流

Stream.generate

通过实现 Supplier 接口,你可以自己来控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。

清单 22. 生成 10 个随机整数

1

2

3

4

5

6

Random seed = new Random();

Supplier<Integer> random = seed::nextInt;

Stream.generate(random).limit(10).forEach(System.out::println);

//Another way

IntStream.generate(() -> (int) (System.nanoTime() % 100)).

limit(10).forEach(System.out::println);

Stream.generate() 还接受自己实现的 Supplier。例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算 Stream 的每个元素值。这些都是维持状态信息的情形。

清单 23. 自实现 Supplier

1

2

3

4

5

6

7

8

9

10

11

Stream.generate(new PersonSupplier()).

limit(10).

forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));

private class PersonSupplier implements Supplier<Person> {

 private int index = 0;

 private Random random = new Random();

 @Override

 public Person get() {

 return new Person(index++, "StormTestUser" + index, random.nextInt(100));

 }

}

输出结果:

1

2

3

4

5

6

7

8

9

10

StormTestUser1, 9

StormTestUser2, 12

StormTestUser3, 88

StormTestUser4, 51

StormTestUser5, 22

StormTestUser6, 28

StormTestUser7, 81

StormTestUser8, 51

StormTestUser9, 4

StormTestUser10, 76

Stream.iterate

iterate 跟 reduce 操作很像,接受一个种子值,和一个 UnaryOperator(例如 f)。然后种子值成为 Stream 的*个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推。

清单 24. 生成一个等差数列

1 Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));.

输出结果:

1 0 3 6 9 12 15 18 21 24 27

与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。

进阶:用 Collectors 来进行 reduction 操作

java.util.stream.Collectors 类的主要作用就是辅助进行各类有用的 reduction 操作,例如转变输出为 Collection,把 Stream 元素进行归组。

groupingBy/partitioningBy

清单 25. 按照年龄归组

1

2

3

4

5

6

7

8

Map<Integer, List<Person>> personGroups = Stream.generate(new PersonSupplier()).

 limit(100).

 collect(Collectors.groupingBy(Person::getAge));

Iterator it = personGroups.entrySet().iterator();

while (it.hasNext()) {

 Map.Entry<Integer, List<Person>> persons = (Map.Entry) it.next();

 System.out.println("Age " + persons.getKey() + " = " + persons.getValue().size());

}

上面的 code,首先生成 100 人的信息,然后按照年龄归组,相同年龄的人放到同一个 list 中,可以看到如下的输出:

1

2

3

4

5

6

7

Age 0 = 2

Age 1 = 2

Age 5 = 2

Age 8 = 1

Age 9 = 1

Age 11 = 2

……

清单 26. 按照未成年人和成年人归组

1

2

3

4

5

Map<Boolean, List<Person>> children = Stream.generate(new PersonSupplier()).

 limit(100).

 collect(Collectors.partitioningBy(p -> p.getAge() < 18));

System.out.println("Children number: " + children.get(true).size());

System.out.println("Adult number: " + children.get(false).size());

输出结果:

1

2

Children number: 23

Adult number: 77

在使用条件“年龄小于 18”进行分组后可以看到,不到 18 岁的未成年人是一组,成年人是另外一组。partitioningBy 其实是一种特殊的 groupingBy,它依照条件测试的是否两种结果来构造返回的数据结构,get(true) 和 get(false) 能即为全部的元素对象。

结束语

总之,Stream 的特性可以归纳为:

  • 不是数据结构
  • 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
  • 它也*不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
  • 所有 Stream 的操作必须以 lambda 表达式为参数
  • 不支持索引访问
  • 你可以请求*个元素,但无法请求第二个,第三个,或*后一个。不过请参阅下一项。
  • 很容易生成数组或者 List
  • 惰性化
  • 很多 Stream 操作是向后延迟的,一直到它弄清楚了*后需要多少数据才会开始。
  • Intermediate 操作永远是惰性化的。
  • 并行能力
  • 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
  • 可以是无限的
    • 集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

浅析DNS域名解析过程

一、DNS域名解析步骤

下图是DNS域名解析的一个示例图,它涵盖了基本解析步骤和原理。
这里写图片描述
下面DNS解析步骤进行讲解,后面将采用命令行的形式来跟踪DNS解析过程。当用户在地址栏键入www.baidu.com并敲下回车键之后,域名解析就开始了。

*步:检查浏览器缓存中是否缓存过该域名对应的IP地址

用户通过浏览器浏览过某网站之后,浏览器就会自动缓存该网站域名对应的IP地址,当用户再次访问的时候,浏览器就会从缓存中查找该域名对应的IP地址,因为缓存不仅是有大小限制,而且还有时间限制(域名被缓存的时间通过TTL属性来设置),所以存在域名对应的IP找不到的情况。当浏览器从缓存中找到了该网站域名对应的IP地址,那么整个DNS解析过程结束,如果没有找到,将进行下一步骤。对于IP的缓存时间问题,不宜设置太长的缓存时间,时间太长,如果域名对应的IP发生变化,那么用户将在一段时间内无法正常访问到网站,如果太短,那么又造成频繁解析域名。

第二步:如果在浏览器缓存中没有找到IP,那么将继续查找本机系统是否缓存过IP

如果*个步骤没有完成对域名的解析过程,那么浏览器会去系统缓存中查找系统是否缓存过这个域名对应的IP地址,也可以理解为系统自己也具备域名解析的基本能力。在Windows系统中,可以通过设置hosts文件来将域名手动绑定到某IP上,hosts文件位置在C:\Windows\System32\drivers\etc\hosts。对于普通用户,并不推荐自己手动绑定域名和IP,对于开发者来说,通过绑定域名和IP,可以轻松切换环境,可以从测试环境切换到开发环境,方便开发和测试。在XP系统中,黑客常常修改他的电脑的hosts文件,将用户常常访问的域名绑定到他指定的IP上,从而实现了本地DNS解析,导致这些域名被劫持。在Linux或者Mac系统中,hosts文件在/etc/hosts,修改该文件也可以实现同样的目的。

前两步都是在本机上完成的,所以没有在上面示例图上展示出来,从第三步开始,才正在地向远程DNS服务器发起解析域名的请求。

第三步:向本地域名解析服务系统发起域名解析的请求

如果在本机上无法完成域名的解析,那么系统只能请求本地域名解析服务系统进行解析,本地域名系统LDNS一般都是本地区的域名服务器,比如你连接的校园网,那么域名解析系统就在你的校园机房里,如果你连接的是电信、移动或者联通的网络,那么本地域名解析服务器就在本地区,由各自的运营商来提供服务。对于本地DNS服务器地址,Windows系统使用命令ipconfig就可以查看,在LinuxMac系统下,直接使用命令cat /etc/resolv.conf来查看LDNS服务地址。LDNS一般都缓存了大部分的域名解析的结果,当然缓存时间也受域名失效时间控制,大部分的解析工作到这里就差不多已经结束了,LDNS负责了大部分的解析工作。

第四步:向根域名解析服务器发起域名解析请求

本地DNS域名解析器还没有完成解析的话,那么本地域名解析服务器将向根域名服务器发起解析请求。

第五步:根域名服务器返回gTLD域名解析服务器地址

本地DNS域名解析向根域名服务器发起解析请求,根域名服务器返回的是所查域的通用顶级域(Generic top-level domain,gTLD)地址,常见的通用顶级域有.com.cn.org.edu等。

第六步:向gTLD服务器发起解析请求

本地域名解析服务器向gTLD服务器发起请求。

第七步:gTLD服务器接收请求并返回Name Server服务器

gTLD服务器接收本地域名服务器发起的请求,并根据需要解析的域名,找到该域名对应的Name Server域名服务器,通常情况下,这个Name Server服务器就是你注册的域名服务器,那么你注册的域名的服务商的服务器将承担起域名解析的任务。

第八步:Name Server服务器返回IP地址给本地服务器

Name Server服务器查找域名对应的IP地址,将IP地址连同TTL值返回给本地域名服务器。

第九步:本地域名服务器缓存解析结果

本地域名服务器缓存解析后的结果,缓存时间由TTL时间来控制。

第十步:返回解析结果给用户

解析结果将直接返回给用户,用户系统将缓存该IP地址,缓存时间由TTL来控制,至此,解析过程结束。

这里对DNS解析的步骤进行了一个简单的介绍分析,后面将通过命令行的形式来解析一个域名的具体解析过程。

二、DNS域名解析过程分析

在正式开始分析解析过程之前,先来介绍几个基本的域名解析方式的概念。域名解析记录主要分为A记录MX记录CNAME记录NS记录以及TXT记录

  • A记录A代表的是Address,用来指定域名对应的IP地址,比如将map.baidu.com指定到180.97.34.157,将zhidao.baidu.com指定到180.149.131.245A记录允许将多个域名解析到一个IP地址,但不允许将一个域名解析到多个IP地址上。
  • MX记录MX代表的是Mail Exchage,就是可以将某个域名下的邮件服务器指向自己的Mail Server,如baidu.com域名的A记录IP地址是180.97.34.157,如果将MX记录设置为180.97.34.154,即xxx@baidu.com的邮件路由,那么DNS会将邮件发送到180.97.34.154所在的服务器,而正常web请求仍然会解析到A记录的IP地址180.97.34.157
  • CNAME记录CNAME指的就是Canonical Name,也就是别名解析,可以将指定的域名解析到其他域名上,而其他域名就是指定域名的别名,整个解析过程称为别名解析。比如将baidu.com解析到itlemon.cn,将csdn.net解析到itlemon.cn,那么itlemon.cn就是baidu.comCSDN.net的别名。
  • NS记录:就是为某个域名指定了特定的DNS服务器去解析。
  • TXT记录:为某个主机名或者域名设置特定的说明,比如为itlemon.cn设置的的TXT记录为“Lemon的技术笔记”,这个TXT记录为itlemon.cn的说明。

上面概念中的IP地址都是假定的,帮助理解。下面将通过解析域名baidu.com为例,进一步说明域名解析流程。

直接查看域名结果,可以通过命令nslookup加上域名来查看:
这里写图片描述
上图中Non-authoritative answer表示解析结果来自非权威服务器,也就是说这个结果来自缓存,并没有完全经历全部的解析过程,从某个缓存中读取的结果,这个结果存在一定的隐患,比如域名对应的IP地址已经更变。
这只是一个快捷的解析结果,如果需要浏览全部的解析过程,那么可以使用dig命令来查看解析过程。
这里写图片描述
分析上图DNS解析过程,我们可以看出:
*步:从本地DNS域名解析服务器获取到13个根DNS域名服务器(.)对应的主机名。
这里写图片描述
第二步:从13个根域名服务器中的其中一个(这里是h.root-servers.net)获取到顶级com.的服务器IP(未显示)和名称。
这里写图片描述
第三步:向com.域的一台服务器192.43.172.30(i.gtld-servers.net)请求解析,它返回了baidu.com域的服务器IP(未显示)和名称,百度有四台顶级域的服务器。
这里写图片描述
第四步:向百度的顶级域服务器220.181.37.10(ns3.baidu.com)请求www.baidu.com,它发现这个www有个别名,而不是一台主机,别名是www.a.shifen.com
这里写图片描述
一般情况下,DNS解析到别名就停止了,返回了具体的IP地址,如果想看到具体的IP地址,可以进一步对别名进行解析,解析结果如下:
这里写图片描述
这时候看到*后的解析结果是180.97.33.107180.97.33.108。在解析别名的过程中,可以发现shifen.combaidu.com都是指定了相同的域名解析服务器。以上是一个域名的解析过程,*后的解析结果和一开始的使用nslookup的结果一致。

HashMap ConcurrentHashMap详解

转载自:https://mp.weixin.qq.com/s/QhRWDFgpjQ83Yz66V_6scQ

前言

 

Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。

 

本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。

 

HashMap

 

众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。

 

Base 1.7

 

1.7 中的数据结构图:

 

 

 

先来看看 1.7 中的实现。

 

 

 

这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?

 

  1. 初始化桶大小,因为底层是数组,所以这是数组默认的大小。
  2. 桶*大值。
  3. 默认的负载因子(0.75)
  4. table 真正存放数据的数组。
  5. Map 存放数量的大小。
  6. 桶大小,可在初始化时显式指定。
  7. 负载因子,可在初始化时显式指定。

 

重点解释下负载因子:

 

由于给定的 HashMap 的容量大小是固定的,比如默认初始化:

 

 

 

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

 

因此通常建议能提前预估 HashMap 的大小*好,尽量的减少扩容带来的性能损耗。

根据代码可以看到其实真正存放数据的是

 

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

 

这个数组,那么它又是如何定义的呢?

 

 

 

Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:

 

  • key 就是写入时的键。
  • value 自然就是值。
  • 开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。
  • hash 存放的是当前 key 的 hashcode。

 

知晓了基本结构,那来看看其中重要的写入、获取函数:

 

put 方法

 

 

 

  • 判断当前数组是否需要初始化。
  • 如果 key 为空,则 put 一个空值进去。
  • 根据 key 计算出 hashcode。
  • 根据计算出的 hashcode 定位出所在桶。
  • 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
  • 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。

 

 

 

当调用 addEntry 写入 Entry 时需要判断是否需要扩容。

 

如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。

 

而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。

 

get 方法

 

再来看看 get 函数:

 

 

 

  • 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
  • 判断该位置是否为链表。
  • 不是链表就根据 key、key 的 hashcode 是否相等来返回值。
  • 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
  • 啥都没取到就直接返回 null 。

Base 1.8

 

不知道 1.7 的实现大家看出需要优化的点没有?

 

其实一个很明显的地方就是:

 

当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。

 

因此 1.8 中重点优化了这个查询效率。

 

1.8 HashMap 结构图:

 

 

 

先来看看几个核心的成员变量:

 

%title插图%num

 

和 1.7 大体上都差不多,还是有几个重要的区别:

 

  • TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
  • HashEntry 修改为 Node。

 

Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。

 

再来看看核心方法。

 

put 方法

 

 

 

看似要比 1.7 的复杂,我们一步步拆解:

 

  1. 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
  2. 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
  3. 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
  4. 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
  5. 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
  6. 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
  7. 如果在遍历过程中找到 key 相同时直接退出遍历。
  8. 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
  9. *后判断是否需要进行扩容。

 

get 方法

 

%title插图%num

 

get 方法看起来就要简单许多了。

 

  • 首先将 key hash 之后取得所定位的桶。
  • 如果桶为空则直接返回 null 。
  • 否则判断桶的*个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
  • 如果*个不匹配,则判断它的下一个是红黑树还是链表。
  • 红黑树就按照树的查找方式返回值。
  • 不然就按照链表的方式遍历匹配返回值。

 

从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。

 

但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。

 

%title插图%num

 

但是为什么呢?简单分析下。

 

看过上文的还记得在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。

 

如下图:

 

%title插图%num

%title插图%num

遍历方式

 

还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种:

 

%title插图%num

 

强烈建议使用*种 EntrySet 进行遍历。

 

*种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。

 

简单总结下 HashMap:无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。

 

因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。

 

坚持看到这里的朋友算是已经把 ConcurrentHashMap 的基础已经打牢了,下面正式开始分析。

ConcurrentHashMap

 

ConcurrentHashMap 同样也分为 1.7 、1.8 版,两者在实现上略有不同。

 

Base 1.7

 

先来看看 1.7 的实现,下面是他的结构图:

 

%title插图%num

 

如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。

 

它的核心成员变量:

 

%title插图%num

 

Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

 

%title插图%num

 

看看其中 HashEntry 的组成:

 

%title插图%num

 

和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。

 

原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

 

下面也来看看核心的 put get 方法。

 

put 方法

 

%title插图%num

 

首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。

 

%title插图%num

%title插图%num

 

虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。

 

首先*步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

 

%title插图%num

 

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

 

%title插图%num

 

再结合图看看 put 的流程。

 

  1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  4. *后会解除在 1 中所获取当前 Segment 的锁。

 

get 方法

 

%title插图%num

 

get 逻辑比较简单:

 

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

 

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是*新值。

 

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

 

Base 1.8

 

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。

 

那就是查询遍历链表效率太低。

 

因此 1.8 做了一些数据结构上的调整。

 

首先来看下底层的组成结构:

 

 

 

看起来是不是和 1.8 HashMap 结构类似?

 

其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

 

 

 

也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。

 

其中的 val next 都用了 volatile 修饰,保证了可见性。

 

put 方法

 

重点来看看 put 函数:

 

 

 

  • 根据 key 计算出 hashcode 。
  • 判断是否需要进行初始化。
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  • 如果都不满足,则利用 synchronized 锁写入数据。
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

 

get 方法

 

 

 

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。

 

1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

总结

 

看完了整个 HashMap 和 ConcurrentHashMap 在 1.7 和 1.8 中不同的实现方式相信大家对他们的理解应该会更加到位。

 

其实这块也是面试的重点内容,通常的套路是:

 

  1. 谈谈你理解的 HashMap,讲讲其中的 get put 过程。
  2. 1.8 做了什么优化?
  3. 是线程安全的嘛?
  4. 不安全会导致哪些问题?
  5. 如何解决?有没有线程安全的并发容器?
  6. ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?

 

这一串问题相信大家仔细看完都能怼回面试官。

 

除了面试会问到之外平时的应用其实也蛮多,像之前谈到的 Guava 中 Cache 的实现就是利用 ConcurrentHashMap 的思想。

 

同时也能学习 JDK 作者大牛们的优化思路以及并发解决方案。

 

其实写这篇的前提是源于 GitHub 上的一个 Issues,也希望大家能参与进来,共同维护好这个项目。