备战面试之JavaSE

1、谈一谈JavaSE中的集合框架

alt

JavaSE中的集合框架主要由三种接口组成,分别是Map、Collection、Iterable接口。

Map(映射)

表示一个将Key映射到Value的对象,Map中不可以包含重复的键值,每个键最多只能对应一个Value,遍历Map的过程中遍历到的元素的顺序即为Map的顺序,有些实现例如HashMap不能保证Map元素的有序性,而有些实现例如TreeMap可以保证。

  • HashMap:底层采用数组+链表的方式实现,存储时使用哈希函数计算哈希码,再由哈希码计算出元素在数组存储的索引,将其存在对应的索引上,因此HashMap的get和put操作很快,由于可能出现哈希碰撞,也就是多个Key值对应一个哈希码,此时需要在数组的一个位置存储多个元素,因此引入链表,将多个元素串联在链表中存入数组,当一个链表的长度超过8时,自动将链表转为红黑树以提升查询效率。
  • TreeMap:TreeMap底层使用红黑树实现,利用红黑树的有序性保证了Map顺序的有序性。

Collection(集合)

一个集合代表了一组对象,也成为元素,有些集合允许包含重复元素又有些不允许,有些集合中的元素时有序的,而有些则是无序的,Collection的有三个子接口,分别是List、Queue和Set。

  • List:List代表一个有序的集合,并且允许集合中又重复的元素,ArrayList和LinkedList是该接口的实现,其中ArrayList的特点是随机查找元素以及单个元素的添加很快,而元素的批量添加和删除操作较慢,其底层采用动态数组实现,是集合框架中的一个实现起来比较简单的类,LinkedList的特点是增加和删除元素很快,随机查找较慢,其底层采用双向链表实现,同时LinkedList实现了Queue接口,具有队列的先入先出的功能。
  • Queue:Queue中的元素按照一定的优先程度排序,可能是按照FIFO、LIFO的方式,或者按照给定的优先级排序,无论以何种方式排序,队顶的元素是下一次将要被取出的元素。LinkedList和PriorityQueue是Queue的两种实现方式,其中LinkedList按照FIFO的方式排序,PriorityQueue按照给定的比较器(Comarator)排序,PriorityQueue也称为优先队列,其底层是使用二叉堆实现,每次插入和删除之后仍然能保证现有元素的有序性。我曾经在项目中使用过优先队列,需求是为生产线上的物料推荐最近的存储位置,我通过遍历所有存储位,并根据位置是否已经放满等条件进行筛选,将符合条件的存储位添加到优先队列中,优先级根据物料和库位的尺寸的匹配程度,以及库位和产线的距离远近定义,遍历结束之后,队顶的元素即为最优的选择。
  • Set:Set表示一个不包含重复元素的集合,有些Set能够保证元素的有序性例如TreeSet,有些不能保证有序性例如HashSet,这两各实现分别使用TreeMap和HashMap实现。

Iterable(可迭代)

从上述介绍中看到,不同的集合底层有不同的实现方式,而在使用这些集合时,对集合的遍历是我们经常会遇到的,实现了Iterable接口的集合就可以通过迭代器遍历集合中的元素,也就是说此接口将集合的遍历操作封装,统一了遍历方式而向使用者隐藏了实现细节。

2、谈一谈ThreadLocal

使用TheadLocal是为了解决线程安全问题,一般情况下可以通过加锁等方式,保障程序执行的原子性、可见性以及有序性来解决线程安全问题,而ThreadLocal提供了一种更为简单有效的解决线程安全问题的思路,就是给每个线程分配一个共享变量的副本,这时候每个线程操作的就是不同的变量,自然就不存在线程安全问题。

相比于加锁,使用ThreadLocal不会阻塞线程,性能更高,但是要注意给每个线程分配的副本,必需是通过深拷贝获得的,如果是通过浅拷贝获得的或者只给分配了共享对象的引用,那其实多个线程还是能够访问到同一块内存,线程安全问题依然存在。

3、谈一谈线程池

alt

Java中的线程池,我曾经使用过的是ThreadPoolExecutor, 这是一个用于并发编程的工具类,它主要具有两个功能,第一个是线程复用,第二个是能将任务的提交和执行解耦。

  • 线程复用:先说线程复用,因为线程属于稀缺资源,频繁的创建和销毁线程,一来是编程繁琐,二来是影响性能,使用线程池就可以避免频频繁的创建和销毁线程,线程池复用线程的方式是在内部保存多个Worker,Worker就是用来执行任务的,并且每个Worker持有一个线程,这些Worker有些是常驻的,也就是在线程池运行期间不会销毁,因此就实现了线程复用。
  • 提交与执行解耦:为了实现任务的提交与执行解耦,线程池内部有一个阻塞队列,用于保存任务,我们只需要把任务提交到阻塞队列里面,Worker就会自动的从队列中取出任务执行,这样就实现了任务提交与执行解耦。

实现线程池需要考虑以下几个方面,当任务提交突然变多,常驻的线程不能及时执行,导致阻塞队列装满,为了应对这种情况,ThreadPoolExecutor内部允许队列转满时创建临时线程执行任务,当然线程不可以无限创建,因此,用核心线程数来规定最多允许多少个常驻线程,用最大线程数规定最多允许创建多少线程包括临时线程和常驻线程。如果任务提交过快,即使线程数已经达到最多也无法即使处理,这时就会拒绝提交的任务,线程池内部也有对应的拒绝策略。还个问题就是临时线程何时销毁,在线程池里临时线程不是执行完任务就马上销毁的,而是有个存活时间,当闲置时间超过时间时才会被销毁。因此我们在创建线程池的时候,传递的参数就有核心线程数、最大线程数、线程存活时间、容纳任务的阻塞队列BolockingQueue。

4、谈一谈JavaSE中的并发容器

JavaSE集合框架中,许多经常使用的容器类并不是线程安全的,比如HashMap、ArrayList,并发容器就是这些类的线程安全版本。比如ConcurrentHashMap就是HashMap的线程安全版本,CopyOnWriteArrayList是ArrayList的线程安全版本,在使用非并发容器发生安全性问题的时候,相比于开发人员自己解决安全性问题,直接替换为发容器要更加方便,并且性能更好。

ConcurrentHashMap

alt ConcurrentHashMap内部使用的数据结构和HashMap是一样的,也是通过数组+链表/红黑树的方式实现,大多数形况下,HashMap出现线程安全问题发生在初始化数组的时候、添加元素的时候以及数组扩容的时候,ConcurrentHashMap就需要从这几个方面解决线程安全问题。

  • 确保初始化时线程安全:ConcurrentHashMap中的数组为延迟初始化,只有第一次使用的时候才会初始化,为了避免多个线程参与初始化引发的线程安全问题,其内部采用CAS算法+双重加锁验证的方式保证线程安全。
  • 确保添加元素时线程安全:ConcurrentHashMap内部数据结构采用数组+链表/红黑树,添加元素的时候,如果没有发生哈希碰撞时,新元素会被添加到数组中,在这种情况下ConcurrentHashMap采用CAS算法保证线程安全的添加元素,那么如果发生了哈希碰撞,此时数组上相应的位置是一个链表或者红黑树,也被称为一个哈希桶,那么此时采用的方式是直接对这个位置上的哈希桶加锁以保证线程安全,相比于对整个数组进行加锁,这种方式锁粒度更细,往哈希桶中添加元素不会影响到其他位置的哈希桶。
  • 确保扩容时线程安全:当发生数组扩容的时候,ConcurrentHashMap不仅没有线程安全问题,甚至可以多个线程同时参与扩容,提高效率,扩容的过程就是创建新的数组,将旧数组中的哈希桶转移到新数组,转移结束后用新数组替代就数组,ConcurrentHashMap为每个参与扩容的数组分配一定数量的哈希桶,多个线程分别执行各自的转移工作,转移工作完成后用新数组替代就数组,这个过程中要解决四个方面的问题,第一个是如何保证每个线程分配的哈希桶没有重叠,第二个是如何保证每个哈希桶在转移期间不被修改,第三个是添加元素的时候正在扩容怎么办,第四个是怎么判断转移是否已经结束。针对第一个问题,ConcurrentHashMap使用加volitale关键字的变量表示未被分配的哈希桶的索引,并用CAS操作原子性地修改它的值,来保证分配的哈希桶没有重叠,针对第二个问题,每个线程在转移哈希桶之前,对哈希桶加锁,针对第三个问题,如果在添加元素的时候,发现数组正在进行扩容,那么就为该线程分配哈希桶,让它先协助完成扩容再添加元素,解决最后一个问题,通过原子性的修改当前正在参与扩容的线程数,当线程数为零说明转移已经结束,这里也是采用CAS操作保证原子性。

总结起来,ConcurrentHashMap内部使用volitale关键字的变量+CAS操作+细粒度加锁的方式保证线程安全,同时采用这种方式,再配合多线程辅助扩容的机制,极大地提升了容器性能,非常适合在并发编程中使用。

CopyOnWriteArrayList

这是ArrayList的线程安全版本,其内部通过使用可重入锁来保证写时线程安全,但是如果只采用加锁的方式,那么在写操作的时候,其他线程的读操作就会被阻塞,直到释放锁,为了解决这个问题,CopyOnWriteArrayList采用读写分离的思想,在写操作的时候,先将数组复制一份,在新数组中写,写完后再替换掉原来的数组,这样的话写操作和读操作实际上就是发生在不同的数组中,因此互相之间不会阻塞,这样做牺牲了部分实时性来换取读取效率,如果经常遍历容器,又要保证线程安全,比较推荐使用CopyOnWriteArrayList。

BlockingQueue(阻塞队列)

除了上面所说的并发容器,还有一类经常使用的并发容器,就是阻塞队列,阻塞队列也是一个队列,它继承了Queue接口,并且支持在取出一个元素的时候能够给等待队列非空,添加一个元素的时候能够等待队列变得可用。那么要实现一个阻塞队列就要考虑两个方面的内容,一个是如何实现这种阻塞功能,另一个是如何保证线程安全,ArrayBlockingQueue是阻塞队列的一个实现,它是一个FIFO的阻塞队列,ArrayBlockingQueue实现阻塞的方式是使用生产者消费者模式,也就是在队列空的时候阻塞消费线程,不为空的时候再唤醒,队列满的时候阻塞生产线程,不满的时候再唤醒, ArrayBlockingList内部利用Condition的await()和signal()实现生产者消费者模式,同时ArrayBlockingQueue内部使用可重入锁保证线程安全。

5、谈一谈JavaSE中的原子类

原子类是Java并发包下面为解决并发安全性问题而设计的一组类,顾名思义使用原子类能够保证操作的原子性,拿AutomicInteger举例,AutomicInteger是Integer的线程安全版本,我们知道整型变量的自增操作由于不是原子性的,所以不是安全的,AutomicInteger的getAndIncrement方法执行的就是整型的值加一,并且是线程安全的,其实现方式是使用CAS算法保证自增的原子性。

全部评论

相关推荐

07-15 00:33
江苏大学 Java
代码飞升:哈哈哈哈评论区三个打广告的
简历中的项目经历要怎么写
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务