文章目录
- 1.HashSet如何检查重复
- 2.comparable和Comparator区别
- 3.ConcurrentHashMap和Hashtable区别
- 4.线程和进程
- 5.并发与并行的区别
- 6.为什么使用多线程
- 7.使用多线程可能带来问题
- 8.线程的生命周期和状态
- 9.什么是上下文切换
- 10.线程死锁
- 11.产生死锁四个条件
- 12.如何避免死锁
1.HashSet如何检查重复
当你把对象加入HashSet时,HashSet会先计算对象的hasdcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同的hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
2.comparable和Comparator区别
- comparable接口实际上是出自java.lang包,它有一个compareTo(Object obj)方法用来排序。
- comparator接口实际上是出自java.util包,它有一个compare(Object obj1,Object obj2)方法用来排序。
3.ConcurrentHashMap和Hashtable区别
ConcurrentHashMap和Hashtable的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable的JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
- 实现线程安全的方式:在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中的一部分数据,多线程访问容器里不同的数据段的数据,就不会存在锁竞争,提高并发访问率。到了JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。(JDK1.6以后对synchronized锁做了很多优化)整个看起来像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容低版本;Hashtable使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。
4.线程和进程
进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
在Java中,当我们启动main函数时其实就是启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。
线程
线程与进程类似,但线程是一个比进程更小的执行单位。一个进程在其执行过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小得多,因此,线程被称为轻量级进程。
区别
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但是不利于资源的管理和保护,而进程相反。
5.并发与并行的区别
- 并发:同一时间段,多个任务都在执行(单位时间内不一定同时执行)。
- 并行:单位时间内,多个任务同时执行。
6.为什么使用多线程
先从总体上来说:
- 从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
深入计算机底层来说: - 单核时代:单核时代多线程主要是为了提高CPU和IO设备的综合利用率。
- 多核时代:多核时代多线程是为了提高CPU和IO设备的综合利用率。
7.使用多线程可能带来问题
并发编程目的就是为了能够提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
8.线程的生命周期和状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称为运行中 |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIMED_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经运行完毕 |
线程创建之后处于NEW(新建)状态,调用start()
方法后开始运行,线程这时候处于READY(可运行)状态。可运行状态的线程获得CPU时间片(timeslice)后就处于RUNNING(运行)状态。当线程执行wait()方法之后,线程进入WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态,而TIME_WAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long millis)方法或wait(long millis)方法可以将Java线程置于TIMED WAITING状态。当超时时间到达后Java线程将会返回到RUNNABLE状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入**BLOCKED(阻塞)状态。线程在执行Runnable的run()方法之后将会进入到TERMINATED(终止)**状态。
9.什么是上下文切换
多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说:当前任务在执行完CPU时间片切换到另一个任务之前会保存自己的状态,以便下次再切换回这个任务时,可以再加载这个人物的状态。任务从保存到再加载的过程就是一次上下文切换。
10.线程死锁
线程死锁描述的就是:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
11.产生死锁四个条件
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
12.如何避免死锁
为了避免死锁,只要破坏产生死锁的四个条件中的一个:
- 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥(临界资源需要互斥访问)。
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。