前言:
接下来我们要了解一下,线程安全的集合类有哪些?什么是死锁以及怎么避免死锁问题。
1.多线程环境使用哈希表
1.1:HashTable
只是简单的把关键方法加上synchronized关键字。
public synchronized V put(K key, V value)
public synchronized V get(Object key)
这个是针对this来进行加锁。当多个线程来访问这个HashTable的时候,无论是啥样的操作,无论是什么样的数据,都会出现锁竞争。
一个HashTable只有一把锁,两个线程访问HashTable 中任意的数据都会出现锁竞争。
这相当于直接针对hashtable对象本身加锁。
如果多线程访问同一个hashtable就会直接造成锁冲突。
size属性也是通过synchronized来控制同步,也是比较慢的。
一旦触发扩容,就有该线程完成整个扩容过程,这个过程会涉及大量的元素拷贝,效率会比较低。这是一次就完成整个扩容。
1.2:ConcurrentHashMap
1.读操作没有加锁(但是使用了volatile保证从内存读取结果),只对写操作进行加锁,加锁的方式仍然是使用synchronized。但只是"锁桶"(用每个链表的头节点作为锁对象)这样减低锁冲突的概率)。
2.充分利用CAS特性,比如size属性通过CAS来更新,避免出现重量级锁的情况。
3.优化了扩容方式:化整为零。
。发现需要扩容的线程,每次操作只搬运一点点,通过多次操作完成整个搬运的过程。同时维护一个新的HashMap和一个旧的,查找的时候即需要查找旧的也要查找新的,插入的时候只插入新的,直到搬运完毕再销毁旧的。
1.3:面试题
hashtable和HashMap,ConcurrentHashMap之间的区别:
HashMap:线程不安全,key允许为null
HashTable:线程安全,使用synchronized锁HashTable对象,效率低,key不允许null.
ConcurrentHashMap:线程安全,使用synchronized锁每个链表头结点,锁冲突概率降低,充分利用CAS机制,优化了扩容机制,key不允许为null.
2:死锁
2.1:死锁是什么
多个线程同时被阻塞,他们中一个或者全部都在等待莫个资源被释放,由于线程被无限期的阻塞,因此程序不可能正常终止。
举一个例子:
你和你朋友一起去吃过桥米线,你首先拿到了辣椒油,你朋友拿到了醋,你们都要拿到醋和辣椒油才会吃。当你要你朋友先给你醋,你朋友要你先给他辣椒油。你们两个就一直坚持不下。这样就一直干着,这就叫死锁。
2.2:死锁的案例
1.一个线程,一把锁,连续加锁两次,如果这把锁是不可重入锁,就会形成死锁。
2.两个线程,先对自己拥有的锁加锁,在获取对方的锁。
public static void main(String[] args) {
Object lajiao =new Object();
Object cu=new Object();
Thread me=new Thread(()->{
synchronized (lajiao) {
System.out.println("me拿到了辣椒油");
try {
Thread.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (cu) {
System.out.println("me拿到醋了");
}
}
});
Thread him=new Thread(()->{
synchronized (cu) {
System.out.println("him拿到醋了");
try {
Thread.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lajiao) {
System.out.println("him拿到辣椒油");
}
}
});
me.start();
him.start();
}
}
3.多个线程多把锁---哲学家就餐问题
每个哲学家只做两件事:思考人生或者吃面条,思考人生的时候就会放下筷子。吃面条就会拿起左右两边的筷子(先拿起左手,再拿起右手)
如果哲学家发现筷子拿不起来(被别人占用),就会阻塞等待。
假设同一个时刻,哲学家同时拿起左手边的筷子,然后再尝试拿右手的筷子,就会发现右手的筷子都被占用,由于哲学家互不相让,这时候就形成了死锁。
2.3:死锁产生的四个必要条件:
互斥使用:即当资源被一个线程使用(占有)时,别的线程不能使用。
例如:线程1拿到了锁,线程2要想拿到锁,只能等着。
不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
例如:线程1拿到了锁,线程2要想要拿到锁,只等等到线程1主动释放锁。
请求和保持:即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
例如:线程1拿到锁A,再尝试获取锁B,但A这把锁还是保持的。(不会因为获取锁B,把锁A给释放了)。
以上三个条件都是锁自身的性质。
循环等待:即存在一个等待队列:p1占有p2的资源,p2占有p3的资源,p3占有p1的资源,这样就会形成一个循环等待。
2.4:如何避免死锁
上面讲了四个形成死锁的必要条件,要四个条件都要满足。但前三个都是锁。是无法动的,所以我们要避免死锁,只能破环循环等待。
破坏循环等待:N个线程尝试获取锁的时候,都按照固定的按标号从小到大的顺序来获取锁,这样就可以避免环路等待。