Java面试笔试指南(六)---容器和多线程

1、Java Collections

Java Collections框架中包含了大量集合接口以及这些接口的实现类和操作它们的算法(排序、查找、反转、替换、复制、取最小元素、取最大元素、线程安全化等),主要提供List、Queue、Set、Stack、Map等数据结构

Collection是这个集合框架的基础,里面存储一组对象,表示不同类型的Collection是,作用只是提供维护一组对象的基本接口

Set:表示数学意义上的集合概念,主要特点集合中的元素不能重复,因此存入Set中的每个元素都必须定义equals()方法来确保对象的唯一性,该接口实现2个类:HashSet和TreeSet

List:有序Collection,按对象进入的顺序保存对象,能够对列表中的每个元素的插入和删除位置进行精确的控制,同时可以保存重复的对象,LinkedList、ArrayList、Vector都实现了这个接口

Map:提供一个从键映射到值的数据结构,用于保存键值对,值可以重复,但键必须是唯一的,键不能重复,HashMap(基于散列)、TreeMap(基于红黑树)、LinkedHashMap(基于链表)、WeakHashMap、IdentityHashMap都实现了这个接口,但是执行效率不完全相同

2、迭代器

迭代器是一个对象,它的工作是遍历并选择序列中的独享,它提供了一种访问一个容器对象中的个元素,而又不暴露该对象内部细节的方法,迭代器创建的代价小,因此迭代器通常被称为轻量级的容器

迭代器使用注意事项:

  1. 使用容器的iterator()方法返回一个Iterator,然后通过Iterator的next()方法返回第一个元素
  2. 使用Iterator的hasNext()方法判断容器中是否还有元素
  3. 通过remove()方法删除迭代器返回的元素

ListIterator只存在于List中,支持在迭代期间想List中添加或删除元素,并且可以在List中双向滚动

Tips:在使用iterator()方法时,经常遇到ConcurrentModificationException异常:

  1. 这是由于在使用Iterator变量容器的同时有对容器做增加或删除操作导致的
  2. 由于多线程操作导致当一个线程使用迭代器变量容器的同时,另一个线程对这个容器进行增加或者删除的操作

解决方案:

  • 在遍历的过程中,把需要删除的对象保存在一个集合中,等遍历结束有调用removeAll()方法来删除,或者使用iter.remove()方法
  • 在JDK1.5中引入了线程安全的容器(如ConcurrentHashMap、CopyOnWriteArrayList等)
  • 在使用迭代器遍历容器时,对容器的操作放在synchronized代码块中
  • -

3、ArrayList、Vector、LinkedList

ArrayList、Vector、LinkedList类都在java.util包中,均未可伸缩数组(可以动态改变长度的数组)

名称 描述 随机访问 插入删除元素 扩容 是否线程安全
ArrayList 基于存储元素的Object[] array来实现的,在内存中开辟连续的空间来存储 1.5倍
Vector 基于存储元素的Object[] array来实现的,在内存中开辟连续的空间来存储 2倍
LinkedList 双向链表实现,对数据的索引需要从列表头开始遍历 动态增长

4、HashMap、Hashtable、TreeMap、WeakHashMap

Map迎来存储键值对,在数组中通过下标来对内容索引,在Map中,通过对象来进行索引,用来索引的对象叫key,其对应的对象叫value

名称 描述 访问速度 是否线程安全 特点 迭代 扩容方式
HashMap 最常用的Map,根据键的HashCode值存储数据 允许null的key(最多能用一个null的key),不允许多条记录的值为null Iterator 默认大小16,2 的指数倍扩容
Hashtable 根据键的HashCode值存储数据 不允许null的key和value Enumeration 默认11,old*2+1的方式扩容
TreeMap 实现了SortMap接口,能够把保存的记录根据键排序,取出来的顺序是排序后的 Iterator
LinkedHashMap 输入输出顺序相同 Iterator
WeakHashMap 与HashMap类似,但是采用弱引用,key不在被外部引用就可以被垃圾回收器回收 Iterator

对线程不安全的Map实现线程安全:

Map map = Collections.synchronizedMAP(New HashMap());

用自定义的类作为HashMap或hashtable的key的注意事项:

  • HashMap或hashtable不能存储重复的key,每个键只能唯一映射一个值,当有重复的键时,不会创建新的映射关系,会更新原来的值
  • 在使用自定义的类的对象作为HashMap的key时,会又一种假象感觉key是可以重复的(避免的方法是要重写自定义类的hashCode和equals方法)

HashMap添加元素的过程:

  1. 调用key的hashCode()方法生成hash值h1,如果h1在HashMap中不存在,直接添加
  2. 如果h1已经存在,找到它,分别调用key的equals()方法判断当前key值是否和已经存在的key值相同
  3. 如果equals()方法返回true,说明当前添加的key已经存在,用新的value更新旧的value
  4. 如果equals()方法返回false,说明key还不存在,添加新的映射关系

当新添加的key 的hash值已经存在,就会产生冲突,处理冲突的方法,开放地址法,在hash法,链地址法(HashMap使用这个)

5、线程、进程

线程是指在执行过程中,能够执行程序代码的一个执行单元,线程的四种状态(运行、就行、挂起、结束)

进程是指一段正在执行的程序,程序执行的最小单位,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段、堆空间)以及一些进程级资源(文件),各个线程拥有自己的栈空间;在操作系统级别上程序的运行都是进程为单位的

使用多线程的优势:

  1. 可以减少程序的响应时间
  2. 与进程相比,线程的创建和切换开销更小
  3. 多CPU或多核计算机本身就能执行多线程
  4. 简化程序结构

同步和异步的区别:
当多个线程需要访问同一个资源时,需要以某种顺序来确保资源在某一时刻只能被一个线程使用,要实现同步操作,必须要获得每一个线程对象的锁,获得了锁保证在同一时刻只有一个线程能够进入临界区(访问互斥的代码块),并且在这个锁释放之前,其他线程就不能再进入这个临界区;实现同步的方式:同步代码块或者同步方法

异步与非阻塞类似,由于每个线程都包含了运行时自身需要的数据或方法,因此,在进行输入输出处理时,不必关系其他线程的状态或行为,也不必等到输入输出处理完毕才返回

6、Java实现多线程

JVM允许应用程序并发地运行多个线程,在Java中多线程实现一般有以下三种方式:

  1. 继承Thread类,重写run方法
  2. 实现Runnable接口,并实现该接口的run()方法
  3. 实现Callable接口,重写call()方法

启动线程的唯一方法是通过Thread类的start()方法,调用start()方法后并不是立即执行多线程代码,而是使得该线程变为可运行状态

Callable与Runnable功能类似,但是前者更强大:

  1. Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能
  2. Callable中的call()方法可以抛出异常Runnable中run()方法没有这个功能
  3. 运行Callable可以拿到一个Future对象(表示异步计算的结果),提供了检查计算是否成功的方法

run()和start()方法的区别:
系统通过调用线程类的start()方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,意味着这个线程可以被JVM调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成时间的操作,run()结束后,线程就终止

如果直接调用线程类的run()方法,会被当作一个普通的函数调用,也就是说start()能够异步的调用run(),直接调用run()是同步的

7、多线程实现同步的方法

当使用多线程访问同一个资源时,非常容易出现线程安全问题,因此需要采用同步机制来解决这个问题,Java主要有3中同步机制:

1、synchronized关键字

每个对象都有一个对象锁与之关联,该锁表明对象在任何时候只允许被一个线程拥有,当一个线程调用对象的synchronized代码时,需要先获得这个锁,然后才能执行相应的代码,执行结束后释放


synchronized方法,在方法声明前加入synchronized关键字,当一个方法的方法体规模很大,把该方法声明为synchronized会大大影响程序的执行效率
synchronized代码块,即可以把任意代码段声明为synchronized,也可以指定上锁的对象,非常灵活

2、 wait()方法和notify()方法

在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并可以调用notify()/notifyAll()方法通知整在等待的其他线程,唤醒等的线程去获得锁,让它们去竞争

3、Lock

JDK5新增了Lock接口,以及它的实现类ReentranLock(重入锁),Lock可以用来实现多线程同步,过程如下:

  1. lock(),以阻塞的方式获得锁,如果获得锁立刻返回,如果未获得,当前线程等待,直到获得锁后返回
  2. tryLock(),以非阻塞的方式获取锁,尝试性的获取一下锁,获取到返回true,否则false
  3. tryLock(long timeout,TimeUnit unit),如果获得锁立即返回true,否则或等待参数指定的实际单元,在等待过程中获得锁立即返回true,如果超时返回false
  4. lockInterruptibly()如果获得锁立即返回,为获得锁当前线程进入休眠,直到获得锁;或者当前线程被别的线程中断(收到一个InterruptedException),lock()获取不到锁会处于阻塞,且忽略interrupt()方法

synchronized与Lock

名称 描述 用法不同 运行方式 性能不同 锁机制不同
synchronized 使用Object对象本身的wait、nitif、notifyAll调度机制 方法、代码块(括号中表示需要锁的对象) 托管给JVM 资源竞争不激烈的情况下性能好 锁和释放都在块结构中,获取多个锁,以相反顺序释放,自动解锁,不会因为异常而不释放锁造成死锁
Lock 使用Condition进行线程之间的调度 显式指定起始位置和终止位置 代码实现,更精确 资源竞争激烈的情况下性能好 手动释放,必须在finally块中释放,否则会引起死锁

不能同时使用这2中同步机制


sleep()和wait()
sleep()是线程暂停执行一段时间的方法,wait()也是线程暂停执行的方式

原理不同:

名称 区别 苏醒方式
sleep Thread的静态方法,线程用来控制自身流程的 计时结束线程自动苏醒
wait Object的方法,用于线程间通信,使当前拥有对象锁的线程等待 直到其他线程调用notify、notifyAll方法才醒来

对锁的处理机制不同:

名称 区别
sleep 不释放锁
wait 释放锁,使得下线程所在对象中的其他synchronized数据可以被其他线程使用

使用区域不同:

名称 区别 是否捕获异常
sleep 任何地方 必须捕获异常
wait 在同步控制方法或同步语句块中使用 不需要捕获异常

sleep()与yield()

名称 优先级 状态 异常
sleep 给其他线程运行机会时,不考虑优先级 进入阻塞状态 抛出InterruptedException
yield 给相同优先级或更高优先级的线程 重新进入可执行状态 没有声明任何异常

sleep指定的时间是线程不会运行的最小时间

join()方法的作用,让调用该方法的线程在执行完run()方法后,在执行join方法后面的代码,将2个线程合并用于实现同步功能

8、终止线程的方式

名称 描述 后果
Thread.stop() 终止线程时会释放已经锁定的所以监视资源 导致程序执行的不确定性
Thread.suspend() 不会释放锁,容易导致死锁
让线程自行结束进入Dead状态 提供某种方式让线程自动结束run()方法 进入Dead状态
让线程自行结束进入Dead状态 使用interrupt()方法,抛出InterruptedException 在run方法中捕获来让线程安全退出

线程因为IO而停止,需要抛出一个IO相关的异常使线程离开run方法


死锁:两个或以上的进行在执行过程中,因为争夺资源而造成的一种相互等待现象

9、守护线程

Java提供2种线程,守护线程和用户线程

守护线程:又被称为服务线程、后台线程,在程序运行是在后台提供一种通用服务的线程,并不属于程序中不可或缺的部分

只要有任何非守护线程在运行,程序就不会终止

守护线程的优先级低,并非只有JVM内部提供,用户在编写程序时也可以设置守护线程(在调用线程的start()方法之前,调用对象的setDaemon(true)方法)

在守护线程中产生的子线程还是守护线程,守护线程的一个典型例子就是垃圾回收器,只要JVM启动,它始终在运行, 实时监控和管理系统中可以被回收的资源

你可能感兴趣的:(Java)