锁策略和死锁问题

这里不是描述的某个特定锁,而是描述的锁的特性,描述的是"一类锁".

乐观锁 vs 悲观锁

乐观锁: 预测在该场景中,不太会出现锁冲突的情况.
悲观锁: 预测在该场景中,非常容易出现锁冲突.
锁冲突: 两个线程尝试获取同一把锁,一个线程能获取成功,另一个线程阻塞等待.

重量级锁 vs 轻量级锁

重量级锁: 加锁的开销比较大(花的时间多,占用系统资源多), 大多是悲观锁.
轻量级锁: 加锁的开销比较小(花的时间少,占用系统资源少), 大多是乐观锁.

自旋锁 vs 挂起等待锁

自旋锁: 是轻量级锁的一种典型实现,在用户态下,通过自旋的方式(while循环), 实现加锁效果的.
自旋锁会消耗一定的cpu资源,但是可以做到最快速度拿到锁.
挂起等待锁: 是重量级锁的一种典型实现, 通过内核态,借助系统提供的锁机制,当出现锁冲突的时候,
会牵引到内核对于线程的调度,使冲突的线程阻塞等待.
挂起等待锁消耗的cpu资源更少,无法保证第一时间拿到锁.

读写锁 vs 互斥锁

读写锁: 很多读数据的线程之间并不互斥,而写操作要求与任何人互斥.
1. 两个线程,一个线程读加锁,另一个线程也是读加锁,不会产生锁竞争.
2. 两个线程,一个线程读加锁,另一个线程是写加锁,会产生锁竞争.
3. 两个线程,一个线程写加锁,另一个线程也是写加锁,会产生锁竞争.
互斥锁: 锁本身是靠互斥性发挥作用的.

公平锁 vs 非公平锁

公平锁: 遵守先来后到的锁.
非公平锁: 那就是不遵守先来后到的锁.
操作系统内部的线程调度是随机的,如果不做任何额外的限制,锁就是非公平锁.
要想实现公平锁,就需要一些额外的数据结构来支持.(需要记录每个线程的阻塞等待的时间).

可重入锁 vs 不可重入锁

不可重入锁: 一个线程,针对同一把锁,连续加锁两次,产生死锁了.
可重入锁: 一个线程,针对同一把锁,连续加锁两次,没产生死锁.
public synchronized void add() {
	synchronized (this) {
		count++;
	}
}
// 先调用方法add(),这里假设加锁成功.
// 接下来进入代码块,再次针对this对象加锁.

我们对this对象加两次锁(如果是静态方法,是针对类对象加锁.普通方法是针对this对象加锁),此时就会产生锁竞争.(这里写的synchronized是可重入锁,并不会产生死锁,只是用它来表示锁)
为什么呢?
在代码块中,this上的锁必须要等到add方法执行完毕释放后,才能释放.可是这里的逻辑是又加了一个锁,add方法没有释放锁,第二次加锁进入阻塞等待,这里就一直等下去了,也就是产生死锁了.

如果是不可重入锁,这把锁不会保存,是哪个线程对它进行加锁,只要它处于加锁状态,
又收到了"加锁"请求,就会拒绝加锁,此时就会产生死锁.
如果是可重入锁,则是会让这把锁保存,是哪个线程对它进行加锁,就会先对比一下,
看看加锁的线程是不是持有这把锁的线程,就可以灵活判定了.
synchronized 是可重入锁.
public synchronized void add() {
	synchronized (this) {
		count++;
		synchronized (this) {
			count++;
		}
	}
}
// 只有第一个synchronized真正加锁了,后面的都只是虚晃一枪,并没有真正加锁.
// 释放锁也是在最外层的synchrinized中释放的.
// 那么我们怎么知道哪个是最后一个锁呢?
// 让锁这里有一个计数器就可以了

死锁

死锁: 多个线程同时被阻塞,它们中的一个或者全部都在等待资源释放.由于线程无限被阻塞,因此程序不可能正常终止.

  1. 一个线程, 一把锁, 是不可重入锁, 该线程连续加锁两次, 就会出现死锁.
  2. 两个线程, 两把锁, 这两个线程先分别获取到一把锁, 然后再同时尝试获取对方的锁.
public class Demo27 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("thread1加入了两把锁");
                }
            }

        });

        Thread thread2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1) {
                    System.out.println("thread2加入了两把锁");
                }
            }

        });
        thread1.start();
        thread2.start();
    }
}

此时代码就会进入阻塞等待,thread1要想结束,就必须获取到locker2, thread2要想结束,就必须获取到locker1,这两个线程互相冲突,构成死锁.
在这里插入图片描述
3. N个线程M把锁
这里有一个哲学家就餐问题.
假设有5个哲学家, 5根筷子,哲学家主要做两件事,

  1. 思考人生,放下筷子.
  2. 吃面,会分别拿起左手和右手的筷子,再去夹面条吃.

基于上述模型,绝大多数情况下,这些哲学家都可以很好的工作的.但是如果出现极端情况,就会出现死锁.比如说,同一时刻,5个哲学家都想吃面,同时拿到左手的筷子,此时拿不到右手的筷子,就会进入阻塞等待,会进入死锁.

死锁产生的必要条件

只要破坏其中任意一个条件,就可以避免出现死锁.

  1. 互斥使用: 一个线程获取到一把锁后,别的线程不能获取到这个锁.(锁的基本特性)
  2. 不可抢占: 锁只能被持有者主动释放, 而不能被其它线程直接抢走.
  3. 请求和保持: 一个线程去尝试获取多把锁, 在获取多把锁的过程中, 会保持对第一把锁的获取状态.
  4. 循环等待: 相当于thread1要想结束,就必须获取到locker2, thread2要想结束,就必须获取到locker1,这种情况.

如何简单的解决死锁问题

针对锁进行编号, 并且规定加锁的顺序.约定,每个线程如果要获取多把锁, 就必须先获取编号小的锁, 后获取编号大的锁.
只要所有的线程加锁顺序,严格遵守上述情形, 就一定不会出现循环等待.
比如在哲学家就餐问题中, 给每个筷子(锁)进行编号, 按照编号从小到大获取,就不会出现循环等待.

小结

本篇博客讲述了关于锁的特性, 有不足的地方还请多多补充, 希望有收获的小伙伴多多支持.

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
死锁和资源争用是多线程编程中常见的问题,如果处理不当会导致程序崩溃或者性能下降。以下是一些避免死锁和资源争用的方法: 1. 合理设计策略:当多个线程需要访问共享资源时,必须使用来控制并发访问。合理的策略可以避免死锁和资源争用的问题。例如,使用粗粒度(对整个对象加)可以避免细粒度(对对象的属性或方法加)带来的死锁和性能问题。 2. 避免嵌套:当一个线程持有一个时,它不能再次申请同一个,否则就会发生死锁。因此,在编写代码时应该避免嵌套的情况。 3. 避免长时间持有:当一个线程长时间持有一个时,其他线程就无法访问共享资源,从而导致资源争用和性能下降。因此,在编写代码时应该尽量减少持有的时间,避免对共享资源的长时间占用。 4. 使用并发集合:并发集合是线程安全的数据结构,可以避免多个线程同时访问同一个数据结构带来的资源争用问题。例如,使用 `ConcurrentHashMap` 替代 `HashMap` 可以避免多线程访问同一个 `HashMap` 对象时带来的性能问题。 5. 使用线程池:线程池可以避免频繁地创建和销毁线程对象带来的性能开销,同时可以控制线程的数量和生命周期,避免出现过多的线程导致资源争用和性能下降。 需要注意的是,以上方法只是避免死锁和资源争用的一些常用方法,实际应用中还需要根据具体场景进行综合考虑和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值