Java基础简历4

Java中HashMap底层实现原理(JDK1.8)源码分析

在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

HashMap实现原理:

通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将k/v传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动 调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将k传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞,hashmap通过链表将产生碰撞冲突的元素组织起来,在Java8中,如果一个bucket中碰撞的元素超过某个限制(默认是8),则使用红黑树替换链表,从而提高速度。

3. 你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点

4. 你知道hash的实现吗?为什么要这样实现?
在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。

5. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。

3. put函数的实现

put函数大致的思路为:

对key的hashCode()做hash,然后再计算index;

如果没碰撞直接放到bucket里;

如果碰撞了,以链表的形式存在buckets后;

如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;

如果节点已经存在就替换old value(保证key的唯一性)

如果bucket满了(超过load factor*current capacity),就要resize。4. get函数的实现

在理解了put之后,get就很简单了。大致思路如下:

bucket里的第一个节点,直接命中;

如果有冲突,则通过key.equals(k)去查找对应的entry
若为树,则在树中通过key.equals(k)查找,O(logn);
若为链表,则在链表中通过key.equals(k)查找,O(n)。

效率低下的HashTable:HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低。因为当一个线程访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程 2不但不能进行put方法添加元素,也不能使用get方法获取元素。所以竞争越激烈效率越低。

ConcurrentHashMap的结构:锁分段技术

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在concurrentHashMap中扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须先获得与它对应的Segment锁。

Java中IO

字节流继承于InputStream OutputStream

字符流继承于InputStreamReader     OutputStreamWriter

字节流与字符流的区别

2.1要把一片二进制数据逐一输出到某个设备中,或者从某个设备中逐一读取一片二进制数据,不管输入输出设备是什么,我们要用统一的方式来完成这些操作,用一种抽象的方式进行描述,这个描述方式为IO流。对应的抽象类为OutputStream和InputStream,不同的实现类就代表不同的输入和输出设备,他们都是针对字节进行操作的。

在应用中,经常要完全是字符的一段文本输出去或读进来,字节流可以吗?计算机一切最终都是二进制的字节形式存在。对于"中国"这些字符,首先要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把他显示为字符,我们需要将字节转换为字符。由于这样的需求很广泛,专门提供了字符流的包装类。

底层设备永远只接受字节数据,有时候要字符设备到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转换成字节,再写入底层设备,这为我们向IO设备写入或读取字符串提供了一点点方便。

JAVA中IO技术:BIO、NIO、AIO

1、同步异步、阻塞非阻塞概念  

     同步和异步是针对应用程序和内核的交互而言的。 阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。 

同步: 指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪

异步:指的是用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)

阻塞:当试图对该文件描述符进行读写时,如果当时没有东西可读,或者暂时不可读,程序就进入等待状态,直到有东西可写为止。

非阻塞 :非阻塞状态下,如果没有东西可读,或者不可写,读写函数马上返回,而不会等待。

2、Java对BIO、NIO、AIO的支持:

Java BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

采用阻塞的方式实现。也就是一个Socket套接字需要使用一个线程来进行处理。发生建立连接、读数据、写数据的操作时,都可能会阻塞。一个线程处理一个socket,如果是Server端,那么在支持并发的连接时,就需要更多的线程来完成这个工作。

Java NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

基于事件驱动思想,采用的是Reactor模式。相对于NIO的一个明显的好处是不需要为每个Socket套接字分配一个线程,而可以在一个线程中处理多个Socket套接字相关的工作。统一通过Reactor对所有客户端的Socket套接字的事件做处理,然后派发到不同的线程中。这样就解决了BIO中为支撑更多的Socket套接字而需要打开更多线程的问题。

 

Java AIO:异步非阻塞,服务器模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

采用Proactor模式。AIO进行读/写操作时,只需要调用相应的read/write方法,并且需要传入CompletionHandler;在动作完成后,会调用CompletionHandler。

3、BIO、BIO、AIO适用场景分析:

BIO方式适用于连接于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持

AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

另外,I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统会比较明显。

当提交一个新任务到线程池时,线程池的处理流程如下

1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池的线程都在执行任务,则进入下个流程。

2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

ThreadPoolExecutor执行execute方法分下面4种情况:

1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(执行这一步骤需要获取全局锁)

2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(执行这一步需要获取全局锁)

4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectExecutionHandler.rejectedExcution()方法。

 

 

 

  • 0
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值