死锁
死锁是多线程编程中常见的问题,当两个或多个线程互相等待对方持有的资源而无法继续执行时,就会发生死锁。这种情况下,程序会陷入无法恢复的状态,造成程序停滞或崩溃。以下是死锁产生的常见原因和解决方案。
死锁产生条件
- 互斥访问资源:多个线程相互竞争访问资源,如果资源被一个线程持有,其他线程无法获取到该资源。
- 不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 循环等待:多个线程形成环形的资源依赖关系,每个线程都在等待下一个线程释放资源。
- 占用并等待:线程在持有一个资源的同时,又请求其他资源,导致其他线程无法继续执行。
当上述四个条件都成立时,死锁变形成了。
可以举例“哲学家就餐问题”,有一群哲学家围着一张桌子吃饭,每两个哲学家之间放一个筷子,哲学家只做两件事:思考人生 或者 吃面条。 思考人生的时候就会放下筷子。吃面条就会拿起左右两边的筷子(先拿起左边, 再拿起右边),哲学家发现筷子拿不起来就会阻塞等待思考人生。五个哲学家同一时刻同时拿起左的筷子,再去拿右边的筷子就会发现筷子已被占有,就会阻塞等待,进行思考人生,哲学家们互相挂起等待就会形成“死锁”。
解决方案
如何解决死锁呢,那就是打破死锁形成的四个必要条件。
- 破坏互斥条件:对于一些非必要的资源,可以改为共享资源,多个线程可以同时访问,从而避免互斥。
- 破坏占用并等待:当线程需要多个资源时,一次性获取所需资源,而不是一个个依次获取,或者采用资源预分配的策略。
- 破坏循环等待:通过对资源进行编号或者按照固定的顺序申请资源,避免形成循环等待。
其中最容易破坏的就是 “循环等待”
破坏循环等待最常用的一种死锁阻止技术就是锁排序。假设有 N 个线程尝试获取 M 把锁,就可以针对 M 把锁进行编号。N 个线程尝试获取锁的时候,都按照固定的按编号由小到大顺序来获取锁。这样就可以避免环路等待。
下面分别演示会产生环路等待的代码与不会产生环路等待的代码。
可能会产生环路等待:
public static void main(String[] args) { Object lock1 = new Object(); Object lock2 = new Object(); Thread t1 = new Thread(){ @Override public void run() { //获取锁 synchronized (lock1){ synchronized (lock2){ } } } }; Thread t2 = new Thread(){ @Override public void run() { synchronized (lock2){ synchronized (lock1){ } } } }; t1.start(); t2.start(); }
不会产生环路等待的代码:
public static void main(String[] args) { Object lock1 = new Object(); Object lock2 = new Object(); Thread t1 = new Thread(){ @Override public void run() { //获取锁 synchronized (lock1){ synchronized (lock2){ } } } }; Thread t2 = new Thread(){ @Override public void run() { synchronized (lock1){ synchronized (lock2){ } } } }; t1.start(); t2.start(); }
约定好顺序,先获取lock1锁再获取lock2锁
HashTable
HashTable只是对关键方法加上了 synchronized 关键字。
这就相当于对hashtable对象直接进行加锁。
- 多个线程访问同一个hashtable就会发送锁冲突
- 由于同步锁的存在,哈希表的性能相对较差。在高并发情况下,多线程的竞争可能会导致性能下降。
- 哈希表不允许键和值为 null。
ConcurrentHashMap
- 每个链表的头结点作为锁对象,降低了锁冲突概率
- 读操作没有加锁,写操作进行了加锁依旧使用synchronized
- 充分利用 CAS 特性
- 优化了扩容方式
- 化整为零,当发现需要扩容时,会创建一个新的数组,并进行一部分的数据搬运
- 插入新元素时就会直接给新表里插入元素,并搬运一部分数据
- 删除元素时,新旧表都会进行查找,元素在那个表上,在那个表上删除
- 直到旧表元素搬运完成,才会把旧表进行删除
只有当两个线程访问同一个哈希桶上的数据才会进行锁冲突。
HashMap
- 线程不安全:哈希映射是非线程安全的,不适用于多线程环境。在多线程环境下使用时,可能导致数据不一致的问题。
- 性能:哈希映射的性能较好,由于没有同步锁的开销,能够更快地执行插入、查找和删除操作。