锁和死锁
锁是Java中用来保证线程操作原子性的一种机制
锁是数据库中用来保证事物操作原子性的一种机制
Java中的锁Synchronized(加锁)和lock的锁
Synchronized是关键字可以锁代码块也可以锁方法
Lock是类(官方推荐)只能锁代码块
我们把数据类型分为线程安全类型和线程不安全类型
如果一个数据类型需要我们手动加锁来保证其操作的原子性,那么他就是线程不安全的数据类型
如果一个数据类型能自己在方法中加锁来保证其操作的原子性,那么他就是线程安全的
线程不安全 | 线程安全 |
ArrayList | 1.Vector 2.CopyOnwriteArrayList |
HashMap | 1.Hashtable 2.ConcurrentHashMap |
String,Stringbuilder | StringBuilder |
Int,Integer | Atomiclnteger |
其他 |
(自己整理)
产生死锁的原因
互斥条件:锁要具有排他性,在同一时刻,锁只能被一个线程持有
请求与保持条件:一个线程因为请求其他资源被阻塞时,对以获取的资源保持不释放
不剥夺条件:一个线程没有主动释放资源之前,是不能被其他线程强行剥夺
循环等待条件:A线程持有资源a的锁,B线程持有资源b的锁,在互相不释放自己持有的锁的情况下,去请求对方持有的锁,这时候会形成双方循环等待,造成永久阻塞
如何解决死锁
破坏一个条件即可
破坏互斥条件:用共享锁,在同一个时刻,锁可以被多个线程持有
破坏请求与保持条件:—次性申请所有的资源。
破坏不剥夺条件:一个线程因为请求其他资源被阻塞时,主动释放以获取的资源
破坏循环等待条件:所有的线程按照同样的顺序请求资源
(官方答案)
形成死锁的四个必要条件是什么
1.互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
2请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
4.循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
如何避免线程死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件
—次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。我们对线程2的代码修改成下面这样就不会产生死锁了。
集合容器
- Collection和Map的区别
Collection和Map是官方提供的集合容器的两大体系的顶层接口
Collection代表单元素集合体系
Map代表kv键值对集合体系
Collection接口继承了iterable迭代器接口,所有的子类都提供了迭代器的实现,Map体系没有
- List,set,queue的区别
List,set,queue都代表collection体系下的子接口,分别代表三个体系
List体系的特点是有序,不唯一
Set体系的特点是无序,唯一
Queue体系特点是先入后出
- 队列和栈的区别
队列是一种FIFO(FirstInFirstOut)先入后出
栈是一种FILO(FirstInLastOut)先入后出
Java集合体系中的LinkedList类可以实现队列和栈结构
在链表头部插入尾部取出或者尾部插入头部取出就是队列(插入和取出在不同方向上进行)
在链表头部插入头部取出或者尾部插入尾部取出就是栈(插入和取出在相同方向上进行)
面试题
Array和ArrayList的区别
ArrayList和LinedLis的区别
ArrayList和Vector的区别(带一下CopyOnWriteArrayList)
- ArrayList是线程不安全的,Vector是线程安全的。ArrayList中所有的方法都没有加同步锁Vector中所有的方法都加了synchronized同步锁。官方在JDK1.5版本中又推出了一个CopyOnwriteArrayList,使用了Lock锁实现线程安全,然后弃用了Vector,因为Lock锁的性能比synchronized锁的性能更好
在并开发编程中,如果多个线程共享一个ArrayList,那么必须考虑线程安全问题,可以自己在代码中对ArrayList操作代码加锁,或者直接用线程安全的CopyOnwriteArrayList类
在不考虑线程安全的环境下,用ArrayList性能更好,因为加锁开锁是很耗性能的
Array怎么转换为ArrayList,ArrayList怎么转换为Array
- 官方提供的数组工具类Array中提供了一个静态方法asList()可以把数组转换为List,参数是数组,返回值是List
- Arrays类中提供了toArray()方法可以把ArrayList转换为Array后进行返回
7.HashSet和TreeSet的区别
HashSet和TreeSet都是Set接口下面的子类
HashSet的底层是HashMap,他们将数据存储在HashMap的key中
HashSet是无序的唯一的,因为HashMap的key是无序,唯一的
TreeSet的底层是TreeMap,他将数据存储在TreeMap的key中
TreeSet是有序的唯一的,因为TreeMap的key是有序,唯一的
8.HashMap和HashTable的区别
HashMap是线程不安全的,HashTable是线程安全的。HashMap中所有的方法都没有加同步锁HashTable中所有的方法都加了synchronized同步锁。官方在JDK1.5版本中又推出了一个CopyOnwriteArrayList,使用了Lock锁实现线程安全,然后弃用了HashTable,因为Lock锁的性能比synchronized锁的性能更好。
在并开发编程中,如果多个线程共享一个HashMap,那么必须考虑线程安全问题,可以自己在代码中对HashMap操作代码加锁,或者直接用线程安全的CopyOnwriteArrayList类
在不考虑线程安全的环境下,用HashMap性能更好,因为加锁开锁是很耗性能的。
对nullkey和Null value的支持:HashMap支持key为null,但只能有一个,Hashtable不支持,会直接抛出NPE。HashMap和Hashtable支持value为空,不限制个数。
HashMap在1.8以后,设置了阈值=8当链表长度超过阈值的时候,会转换为红黑树以减少检索的时间HashTable被弃用,没有更新
初始容量大小和扩容大小的区别
HashMap初始容量是16,扩容策略是原来的2倍
HashTable初始容量是11,扩容策略是原来的2n+1
HashMap如果手动指定了初始容量,不是2的n次方,他会找到最近的一个2 的n次方作为初始容量
HashTable如果手动指定了初始容量,会直接使用给定的大小
Hashtable采用的锁全表的机制,CopyOnwriteArrayList采用了分段频的设计,锁粒度更细,性能更好。
- HashMap和TreeMap的区别
HashMap底层是数组+链表/红黑树,key是无序的,唯一的
TreeMap底层是红黑树,key是有序的,唯一的
HashMap的性能比TreeMap更好 单如果需要一个有序的key的集合,需要使用TreeMap
10.HashMap的底层原理(数据结构+put()流程+resize()流程)
HashMap在]DK1.8之前是数组+链表,JDK1.8之后是数组+链表/红黑树HashSet的底层 是HashMap
1.根据key的hashCode计算出数组index
2.落槽时
1.如果数组中节点为null,创建新的节点对象,把k,v存储在节点对象中,把节点对象存储在数组中
2.如果数组的节点不为null,判断节点的key与插入元素的key是否相等
1.相等,直接用新的k ,v覆盖原节点中的k,v
2.不相等,判断此时节点是否为红黑树
1.是红黑树,创建红黑树节点对象存储k,v,插入到红黑树中
2.不是红黑树,创建链表节点对象存储k,v,插入到链表中,判断链表长度是否大于阈值8
1.大于阈值8,链表转换为红黑树
3.判断++size是否大于阈值,是就扩容
- 什么是哈希碰撞哈希冲突如何解决哈希冲突
如果有两个不同字符串通过同样的哈希算法计算出来的哈希码是一样的,则称他们发生了哈希碰撞,哈希冲突,解决方法:
1.开放地址法
2.拉链法(链地址法)HashMap默认使用的就是这种
- HashMap为什么不直接使用key的hashCode()函数返回的哈希码作为落槽时的索引号, HashMap是怎么解决的呢
这题也可以这样问"HashMap的底层是如何计算key落槽时的索引的”
hashCode()方法返回的是 int整数类型,其范围为-(2^31)~(2^31-1),约有40亿个映射空间,而 HashMap的容量范围是在16(初始化默认值)~2N 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过`hashCode()`计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
HashMap自己实现了自己的 hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均。
- == equals 的区别
==和equal()都可以用来比较,语法是a==b或者a.equal(b)
==比较的是内存地址
equal()方法是Object类中方法,可以被任意类继承或重写通过看官方源码知道
默认也是用==比较内存地址
如果想要修改equal()方法的比较规则,由默认的比较两个字符串对象的内存地址,修改为比较字符串中每个字符是否相等。
因为堆区中可能会出现两个一模一样的字符串对象,内存地址不一样,所以字符串的比较必须用equal()方法,否则可能会出现内容一模一样的字符串,因为地址不一样比较后出现不相等的情况。
- 为什么重写hashCode(),必须也要写重写equals()方法
HashMap的底层采用了key'的hashcode()来计算数组的索引index
如果数组[index]为空的话说明key不存在,直接落槽插入
如果数组[index]不为null说明位置有key存在,但不能一定说明已存在的key和要插入的key重复的,因为可能发生哈希碰撞,此时应该进一步用equals方法比较已存在的key要插入key是否相等。如果相等就说明一定是重复的,应该覆盖,如果不相等,说明发生了哈希碰撞,那么应该插入链表中
重写equal方法的目的是为了不去比较两个对象的内存地址,改为比较对象的内容,如果一个类重写了equal,没有重写hashcode,就可能出现两个地址不同的对象equal比较相等,
但是hashcode比较不相等,这样会为发hashMap的唯一性,因此,重写了equal方法必须也要重写hashcode方法,且满足两个对象equal相等,hashcode也相等。