105道Java面试题,认真思考对你面试很有价值

1、什么是面向对象?

2、JDK JRE JVM

3、==和equals比较

4、hashCode与equals

5、final

6、String、StringBuffer、StringBuilder

7、重载和重写的区别

8、接口和抽象类的区别

9、List和Set的区别

10、ArrayList和LinkedList区别

11、HashMap和HashTable有什么区别?其底层实现是什么?

12、ConcurrentHashMap原理,jdk7和jdk8版本的区别?

13、什么是字节码?采用字节码的好处是什么?

14、Java中的异常体系

15、Java类加载器

16、双亲委托模型

17、GC如何判断对象可以被回收

选择Java基础里的其中一道解答

JDK JRE JVM

JDK:

Java Develpment Kit java 开发工具

JRE:

Java Runtime Environment java运行时环境

JVM:

java Virtual Machine java 虚拟机

1、线程的生命周期?线程有几种状态

2、sleep()、wait()、join()、yield()的区别

3、对线程安全的理解

4、Thread、Runable的区别

5、对守护线程的理解

6、ThreadLocal的原理和使用场景

7、ThreadLocal内存泄露原因,如何避免

8、并发、并行、串行的区别

9、并发的三大特性

10、volatile

11、为什么用线程池?解释下线程池参数?

12、简述线程池处理流程

13、线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?

14、线程池中线程复用原理

选择Java线程与并发里的其中一道解答

volatile

(1)保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。

如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

(2)禁止指令重排序优化。

write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。

但是用volatile修饰之后就变得不一样了

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主 存读取。

inc++; 其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以volatile不能保证线程安全。

1、如何实现一个IOC容器

2、spring是什么?

3、谈谈你对AOP的理解

4、谈谈你对IOC的理解

5、BeanFactory和ApplicationContext有什么区别?

6、描述一下Spring Bean的生命周期?

7、解释下Spring支持的几种bean的作用域。

8、Spring框架中的单例Bean是线程安全的么?

9、Spring 框架中都用到了哪些设计模式?

10、Spring事务的实现方式和原理以及隔离级别?

11、spring事务传播机制

12、spring事务什么时候会失效?

13、什么是bean的自动装配,有哪些方式?

选择spring里的其中一道解答

谈谈你对AOP的理解

系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。

当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从 上到下的关系,但并不适合定义从左到右的关系。例如日志功能。

日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。

在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情

1、Spring Boot、Spring MVC 和 Spring 有什么区别

2、SpringMVC 工作流程

3、Spring MVC的主要组件?

4、Spring Boot 自动配置原理?

5、如何理解 Spring Boot 中的 Starter

6、什么是嵌入式服务器?为什么要使用嵌入式服务器?

7、mybatis的优缺点

8、MyBatis 与Hibernate 有哪些不同?

9、#{}和${}的区别是什么?

10、简述 Mybatis 的插件运行原理,如何编写一个插件。

选择springmvc、springBoot、Mybatis里的其中一道解答

SpringMVC 工作流程

1)用户发送请求至前端控制器 DispatcherServlet。

2)DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。

3)处理器映射器找到具体的处理器(可以根据 xml 配置、注解进行查找),生成处理器及处理器(如果有则生成)一并返回给 DispatcherServlet。

4)DispatcherServlet 调用 HandlerAdapter 处理器适配器。

5)HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)

6)Controller 执行完成返回 ModelAndView。

7)HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet。8)DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器。

9)ViewReslover 解析后返回具体 View。

10)DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。

11)DispatcherServlet 响应用户。

1、索引的基本原理

2、mysql聚簇和非聚簇索引的区别

3、mysql索引的数据结构,各自优劣

4、索引设计的原则?

5、什么是最左前缀原则?什么是最左匹配原则

6、锁的类型有哪些

7、InnoDB存储引擎的锁的算法

8、关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?

9、事务的基本特性和隔离级别

10、ACID靠什么保证的?

11、什么是MVCC

12、分表后非sharding_key的查询怎么处理,分表后的排序?

13、mysql主从同步原理

14、简述MyISAM和InnoDB的区别

15、简述mysql中索引类型及对数据库的性能的影响

16、mysql执行计划怎么看

选择MySQL里的其中一道解答

索引的基本原理

索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。

索引的原理:就是把无序的数据变成有序的查询

(1) 把创建了索引的列的内容进行排序

(2)对排序结果生成倒排表

(3)在倒排表内容上拼上数据地址链

(4)在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

1、RDB 和 AOF 机制

2、Redis的过期键的删除策略

3、Redis线程模型、单线程快的原因

4、简述Redis事务实现

5、redis集群方案

6、redis 主从复制的核心原理

7、缓存雪崩、缓存穿透、缓存击穿

选择Redis里的其中一道解答

Redis的过期键的删除策略

Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的 指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略。

1、CAP理论,BASE理论

2、负载均衡算法、类型

3、分布式架构下,Session 共享有什么方案

4、简述你对RPC、RMI的理解

5、分布式id生成方案

6、分布式锁解决方案

7、分布式事务解决方案

8、如何实现接口的幂等性

9、简述ZAB 协议

10、简述zk的命名服务、配置管理、集群管理

11、讲下Zookeeper watch机制

12、zk和eureka的区别

13、Spring Cloud和Dubbo的区别

14、什么是Hystrix?简述实现机制

15、springcloud核心组件及其作用

16、Dubbo 的整体架构设计及分层

选择分布式与微服务里的其中一道解答

Spring Cloud和Dubbo的区别

底层协议:springcloud基于http协议,dubbo基于Tcp协议,决定了dubbo的性能相对会比较好

注册中心:Spring Cloud 使用的 eureka ,dubbo推荐使用zookeeper

模型定义:dubbo 将一个接口定义为一个服务,SpringCloud 则是将一个应用定义为一个服务

SpringCloud是一个生态,而Dubbo是SpringCloud生态中关于服务调用一种解决方案(服务治理)

1、简述RabbitMQ的架构设计

2、RabbitMQ如何确保消息发送 ? 消息接收?

3、RabbitMQ事务消息

4、RabbitMQ死信队列、延时队列

5、RabbitMQ镜像队列机制

6、简述kafka架构设计

7、kafka怎么处理消息顺序、重复发送、重复消费、消息丢失

8、Kafka在什么情况下会出现消息丢失及解决方案?

9、Kafka是pull?push?优劣势分析

10、Kafka中zk的作用

11、简述kafka的rebalance机制

12、Kafka的性能好在什么地方

选择分布式与微服务里的其中一道解答

简述kafka架构设计

Consumer Group:消费者组,消费者组内每个消费者负责消费不同分区的数据,提高消费能力。逻 辑上的一个订阅者。

Topic:可以理解为一个队列,Topic 将消息分类,生产者和消费者面向的是同一个 Topic。

Partition:为了实现扩展性,提高并发能力,一个Topic 以多个Partition的方式分布到多个 Broker上,每个 Partition 是一个 有序的队列。一个 Topic 的每个Partition都有若干个副本(Replica),一个Leader 和若干个 Follower。生产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。Follower负责实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个Follower 还会成为新的 Leader。

Offset:消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从 消费位置继续消费。

Zookeeper:Kafka 集群能够正常工作,需要依赖于 Zookeeper,Zookeeper 帮助 Kafka 存储和管理 集群信息。

这105道Java面试题的答案已经整理成了PDF文档,如下图:

获取方式:关注小编+转发文章+私信【105】获取105道Java面试题文档

希望能帮助到你在面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

重要的事情说三遍,转发+转发+转发,一定要记得点赞转发哦!!!

万字总结!Java集合面试题(含答案,收藏版)

本文目录

  • 常见的集合有哪些?
  • List 、Set和Map 的区别
  • ArrayList 了解吗?
  • ArrayList 的扩容机制?
  • 怎么在遍历 ArrayList 时移除一个元素?
  • Arraylist 和 Vector 的区别
  • Arraylist 与 LinkedList 区别
  • HashMap 解决hash冲突的办法有哪些?HashMap用的哪种?使用的hash算法?扩容过程?put方法流程?红黑树的特点?为什么使用红黑树而不使用AVL树?在解决 hash 冲突的时候,为什么选择先用链表,再转红黑树?HashMap 的长度为什么是 2 的幂次方?HashMap默认加载因子是多少?为什么是 0.75?一般用什么作为HashMap的key?HashMap为什么线程不安全?HashMap和HashTable的区别?
  • LinkedHashMap底层原理?
  • 讲一下TreeMap?
  • HashSet底层原理?
  • HashSet、LinkedHashSet 和 TreeSet 的区别?
  • 什么是fail fast?
  • 什么是fail safe?
  • 讲一下ArrayDeque?
  • 哪些集合类是线程安全的?哪些不安全?
  • 迭代器 Iterator 是什么?
  • Iterator 和 ListIterator 有什么区别?
  • 并发容器 ConcurrentHashMap put执行流程?怎么扩容?ConcurrentHashMap 和 Hashtable 的区别? CopyOnWriteConcurrentLinkedQueue阻塞队列 JDK提供的阻塞队列原理

Java集合类主要由两个接口CollectionMap派生出来的,Collection有三个子接口:List、Set、Queue。

Java集合框架图如下:

List代表了有序可重复集合,可直接根据元素的索引来访问;Set代表无序不可重复集合,只能根据元素本身来访问;Queue是队列集合。Map代表的是存储key-value对的集合,可根据元素的key来访问value。

集合体系中常用的实现类有ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等实现类。

  • List 以索引来存取元素,有序的,元素是允许重复的,可以插入多个null;
  • Set 不能存放重复元素,无序的,只允许一个null;
  • Map 保存键值对映射;
  • List 底层实现有数组、链表两种方式;Set、Map 容器有基于哈希存储和红黑树两种方式实现;
  • Set 基于 Map 实现,Set 里的元素值就是 Map的键值。

ArrayList 的底层是动态数组,它的容量能动态增长。在添加大量元素前,应用可以使用ensureCapacity操作增加 ArrayList 实例的容量。ArrayList 继承了 AbstractList ,并实现了 List 接口。

ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。默认情况下,新的容量会是原容量的1.5倍。以JDK1.8为例说明:

foreach删除会导致快速失败问题,可以使用迭代器的 remove() 方法。

  1. ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。
  2. Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为操作Vector效率比较低。
  1. ArrayList基于动态数组实现;LinkedList基于链表实现。
  2. 对于随机index访问的get和set方法,ArrayList的速度要优于LinkedList。因为ArrayList直接通过数组下标直接找到元素;LinkedList要移动指针遍历每个元素直到找到为止。
  3. 新增和删除元素,LinkedList的速度要优于ArrayList。因为ArrayList在新增和删除元素时,可能扩容和复制数组;LinkedList实例化对象需要时间外,只需要修改指针即可。

HashMap 使用数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的, 链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树,红黑树节点个数小于6(UNTREEIFY_THRESHOLD)时才转化为链表,防止频繁的转化。

解决Hash冲突方法有:开放定址法、再哈希法、链地址法。HashMap中采用的是 链地址法 。

  • 开放定址法基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。
  • 再哈希法提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。
  • 链地址法将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。

Hash算法:取key的hashCode值、高位运算、取模运算。

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:这么做可以在数组比较小的时候,也能保证考虑到高低位都参与到Hash的计算中,可以减少冲突,同时不会有太大的开销。

1.8扩容机制:当元素个数大于threshold时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。

1.7链表新节点采用的是头插法,这样在线程一扩容迁移元素时,会将元素顺序改变,导致两个线程中出现元素的相互指向而形成循环链表,1.8采用了尾插法,避免了这种情况的发生。

原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据e.hash & (oldCap – 1) == 0判断) 。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket。

  1. 如果table没有初始化就先进行初始化过程
  2. 使用hash算法计算key的索引
  3. 判断索引处有没有存在元素,没有就直接插入
  4. 如果索引处存在元素,则遍历插入,有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
  5. 链表的数量大于阈值8,就要转换成红黑树的结构
  6. 添加成功后会检查是否需要扩容
  • 每个节点或者是黑色,或者是红色。
  • 根节点是黑色。
  • 每个叶子节点(NIL)是黑色。
  • 如果一个节点是红色的,则它的子节点必须是黑色的。
  • 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

ConcurrentHashMap 在put的时候会加锁,使用红黑树插入速度更快,可以减少等待锁释放的时间。红黑树是对AVL树的优化,只要求部分平衡,用非严格的平衡来换取增删节点时候旋转次数的降低,提高了插入和删除的性能。

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,链表结构可以保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是插入和删除节点的效率变慢了。如果一开始就用红黑树结构,元素太少,插入和删除节点的效率又比较慢,浪费性能。

Hash 值的范围值比较大,使用之前需要先对数组的长度取模运算,得到的余数才是元素存放的位置也就是对应的数组下标。这个数组下标的计算方法是(n – 1) & hash。将HashMap的长度定为2 的幂次方,这样就可以使用(n – 1)&hash位运算代替%取余的操作,提高性能。

先看下HashMap的默认构造函数:

Node[] table的初始化长度length为16,默认的loadFactor是0.75,0.75是对空间和时间效率的一个平衡选择,根据泊松分布,loadFactor 取0.75碰撞最小。一般不会修改,除非在时间和空间比较特殊的情况下 :

  • 如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值 。
  • 如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

一般用Integer、String 这种不可变类当 HashMap 当 key。String类比较常用。

  • 因为 String 是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就是 HashMap 中的key经常使用字符串的原因。
  • 获取对象的时候要用到 equals() 和 hashCode() 方法,而Integer、String这些类都已经重写了 hashCode() 以及 equals() 方法,不需要自己去重写这两个方法。
  • 多线程下扩容死循环。JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。
  • 在JDK1.8中,在多线程环境下,会发生数据覆盖的情况。

HashMap和Hashtable都实现了Map接口。

  1. HashMap可以接受为null的key和value,key为null的键值对放在下标为0的头结点的链表中,而Hashtable则不行。
  2. HashMap是非线程安全的,HashTable是线程安全的。Jdk1.5提供了ConcurrentHashMap,它是HashTable的替代。
  3. Hashtable很多方法是同步方法,在单线程环境下它比HashMap要慢。
  4. 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

HashMap是无序的,迭代HashMap所得到元素的顺序并不是它们最初放到HashMap的顺序,即不能保持它们的插入顺序。

LinkedHashMap继承于HashMap,是HashMap和LinkedList的融合体,具备两者的特性。每次put操作都会将entry插入到双向链表的尾部。

linkedhashmap

TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序。

TreeMap 的继承结构:

TreeMap的特点:

  1. TreeMap是有序的key-value集合,通过红黑树实现。根据键的自然顺序进行排序或根据提供的Comparator进行排序。
  2. TreeMap继承了AbstractMap,实现了NavigableMap接口,支持一系列的导航方法,给定具体搜索目标,可以返回最接近的匹配项。如floorEntry()、ceilingEntry()分别返回小于等于、大于等于给定键关联的Map.Entry()对象,不存在则返回null。lowerKey()、floorKey、ceilingKey、higherKey()只返回关联的key。

HashSet 基于 HashMap 实现。放入HashSet中的元素实际上由HashMap的key来保存,而HashMap的value则存储了一个静态的Object对象。

HashSetSet 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;

LinkedHashSetHashSet 的子类,能够按照添加的顺序遍历;

TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式可以自定义。

fast-fail是Java集合的一种错误机制。当多个线程对同一个集合进行操作时,就有可能会产生fast-fail事件。例如:当线程a正通过iterator遍历集合时,另一个线程b修改了集合的内容,此时modCount(记录集合操作过程的修改次数)会加1,不等于expectedModCount,那么线程a访问集合的时候,就会抛出ConcurrentModificationException,产生fast-fail事件。边遍历边修改集合也会产生fast-fail事件。

解决方法:

  • 使用Colletions.synchronizedList方法或在修改集合内容的地方加上synchronized。这样的话,增删集合内容的同步锁会阻塞遍历操作,影响性能。
  • 使用CopyOnWriteArrayList来替换ArrayList。在对CopyOnWriteArrayList进行修改操作的时候,会拷贝一个新的数组,对新的数组进行操作,操作完成后再把引用移到新的数组。

采用安全失败机制的集合容器,在遍历时不是直接在集合内容问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

ArrayDeque实现了双端队列,内部使用循环数组实现,默认大小为16。它的特点有:

  1. 在两端添加、删除元素的效率较高
  2. 根据元素内容查找和删除的效率比较低。
  3. 没有索引位置的概念,不能根据索引位置进行操作。

ArrayDeque和LinkedList都实现了Deque接口,如果只需要从两端进行操作,ArrayDeque效率更高一些。如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除(LinkedList有相应的 api,如add(int index, E e)),则应该选LinkedList。

ArrayDeque和LinkedList都是线程不安全的,可以使用Collections工具类中synchronizedXxx()转换成线程同步。

线性安全的集合类:

  • Vector:比ArrayList多了同步机制。
  • Hashtable。
  • ConcurrentHashMap:是一种高效并且线程安全的集合。
  • Stack:栈,也是线程安全的,继承于Vector。

线性不安全的集合类:

  • Hashmap
  • Arraylist
  • LinkedList
  • HashSet
  • TreeSet
  • TreeMap

Iterator模式用同一种逻辑来遍历集合。它可以把访问逻辑从不同类型的集合类中抽象出来,不需要了解集合内部实现便可以遍历集合元素,统一使用 Iterator 提供的接口去遍历。它的特点是更加安全,因为它可以保证,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

主要有三个方法:hasNext()、next()和remove()。

ListIterator 是 Iterator的增强版。

  • ListIterator遍历可以是逆向的,因为有previous()和hasPrevious()方法,而Iterator不可以。
  • ListIterator有add()方法,可以向List添加对象,而Iterator却不能。
  • ListIterator可以定位当前的索引位置,因为有nextIndex()和previousIndex()方法,而Iterator不可以。
  • ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。
  • ListIterator只能用于遍历List及其子类,Iterator可用来遍历所有集合。

JDK 提供的这些容器大部分在 java.util.concurrent 包中。

  • ConcurrentHashMap: 线程安全的 HashMap
  • CopyOnWriteArrayList: 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector.
  • ConcurrentLinkedQueue: 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
  • BlockingQueue: 阻塞队列接口,JDK 内部通过链表、数组等方式实现了这个接口。非常适合用于作为数据共享的通道。
  • ConcurrentSkipListMap: 跳表的实现。使用跳表的数据结构进行快速查找。

多线程环境下,使用Hashmap进行put操作会引起死循环,应该使用支持多线程的 ConcurrentHashMap。

JDK1.8 ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构采用数组+链表/红黑二叉树。synchronized只锁定当前链表或红黑二叉树的首节点,相比1.7锁定HashEntry数组,锁粒度更小,支持更高的并发量。当链表长度过长时,Node会转换成TreeNode,提高查找速度。

在put的时候需要锁住Segment,保证并发安全。调用get的时候不加锁,因为node数组成员val和指针next是用volatile修饰的,更改后的值会立刻刷新到主存中,保证了可见性,node数组table也用volatile修饰,保证在运行过程对其他线程具有可见性。

put 操作流程:

  1. 如果table没有初始化就先进行初始化过程
  2. 使用hash算法计算key的位置
  3. 如果这个位置为空则直接CAS插入,如果不为空的话,则取出这个节点来
  4. 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
  5. 如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作,这里有两种情况,一种是链表形式就直接遍历到尾端插入或者覆盖掉相同的key,一种是红黑树就按照红黑树结构插入
  6. 链表的数量大于阈值8,就会转换成红黑树的结构或者进行扩容(table长度小于64)
  7. 添加成功后会检查是否需要扩容

数组扩容transfer方法中会设置一个步长,表示一个线程处理的数组长度,最小值是16。在一个步长范围内只有一个线程会对其进行复制移动操作。

  1. Hashtable通过使用synchronized修饰方法的方式来实现多线程同步,因此,Hashtable的同步会锁住整个数组。在高并发的情况下,性能会非常差。ConcurrentHashMap采用了更细粒度的锁来提高在并发情况下的效率。注:Synchronized容器(同步容器)也是通过synchronized关键字来实现线程安全,在使用的时候会对所有的数据加锁。
  2. Hashtable默认的大小为11,当达到阈值后,每次按照下面的公式对容量进行扩充:newCapacity = oldCapacity * 2 + 1。ConcurrentHashMap默认大小是16,扩容时容量扩大为原来的2倍。

写时复制。当我们往容器添加元素时,不直接往容器添加,而是先将当前容器进行复制,复制出一个新的容器,然后往新的容器添加元素,添加完元素之后,再将原容器的引用指向新容器。这样做的好处就是可以对CopyOnWrite容器进行并发的读而不需要加锁,因为当前容器不会被修改。

从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。

CopyOnWriteArrayList中add方法添加的时候是需要加锁的,保证同步,避免了多线程写的时候复制出多个副本。读的时候不需要加锁,如果读的时候有其他线程正在向CopyOnWriteArrayList添加数据,还是可以读到旧的数据。

缺点:

  • 内存占用问题。由于CopyOnWrite的写时复制机制,在进行写操作的时候,内存里会同时驻扎两个对象的内存。
  • CopyOnWrite容器不能保证数据的实时一致性,可能读取到旧数据。

非阻塞队列。高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,通过 CAS 操作实现。

如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。适合在对性能要求相对较高,同时有多个线程对队列进行读写的场景。

非阻塞队列中的几种主要方法: add(E e) : 将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常; remove() :移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常; offer(E e) :将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false; poll() :移除并获取队首元素,若成功,则返回队首元素;否则返回null; peek() :获取队首元素,若成功,则返回队首元素;否则返回null

对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。

阻塞队列是java.util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。BlockingQueue 适合用于作为数据共享的通道。

使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。

阻塞队列和一般的队列的区别就在于:

  1. 多线程支持,多个线程可以安全的访问队列
  2. 阻塞操作,当队列为空的时候,消费线程会阻塞等待队列不为空;当队列满了的时候,生产线程就会阻塞直到队列不满。

方法

方法\\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出 插入方法 add(e) offer(e) put(e) offer(e,time,unit) 移除方法 remove() poll() take() poll(time,unit) 检查方法 element() peek() 不可用 不可用

JDK 7 提供了7个阻塞队列,如下

1、ArrayBlockingQueue

有界阻塞队列,底层采用数组实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不能保证线程访问队列的公平性,参数fair可用于设置线程是否公平访问队列。为了保证公平性,通常会降低吞吐量。

2、LinkedBlockingQueue

LinkedBlockingQueue是一个用单向链表实现的有界阻塞队列,可以当做队列也可以当做有界队列来使用。通常在创建 LinkedBlockingQueue 对象时,会指定队列最大的容量。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。与 ArrayBlockingQueue 相比起来具有更高的吞吐量。

3、PriorityBlockingQueue

支持优先级的阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来进行排序。

PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容

PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是队列(take 方法在队列为空的时候会阻塞)。

4、DelayQueue

支持延时获取元素的阻塞队列。队列使用PriorityBlockingQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

5、SynchronousQueue

不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。支持公平访问队列。

SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。

6、LinkedTransferQueue

由链表结构组成的阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法。

transfer方法:如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。

tryTransfer方法:用来试探生产者传入的元素能否直接传给消费者。如果没有消费者在等待,则返回false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

JDK使用通知模式实现阻塞队列。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

ArrayBlockingQueue使用Condition来实现:

文章对你有用的话,点个赞,支持一下~

我是大彬,非科班转码,校招拿了多家互联网中大厂offer,专注分享Java技术干货,欢迎关注~

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。