线程的死锁、活锁和饥饿现象

目录

1、死锁

2、活锁

3、饥饿


一个资源应该单独使用一把锁。

比如,一个对象中有多个共享资源,但有多个线程需要使用其中的不同资源

此时如果把对象整体作为一把锁,那并发就很低。

可以考虑,把每个共享资源都单独拆出来,分别上锁,这样每个线程都能各取所需,提高了并发度。

坏处是,可能导致线程死锁。

线程的代码是有限的,但由于某种原因,线程一直执行不完,称为线程的活跃性。

活跃性有三种原因:死锁、活锁、饥饿。

1、死锁

1、死锁的定义

死锁:一组互相竞争资源的线程因互相等待获取对方的资源,导致线程一直阻塞的情况。

2、为什么会产生死锁

一个线程需要同时获取多把锁时,就容易引发死锁。

简单的例子:

  • t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁
  • t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

它俩又不会释放自己已经持有的锁,一直耗着,产生了死锁。

总结

如果锁应用不当,造成“两个线程都在等待对方释放锁”,那么它们会一直等待,无法运行,这就发生了死锁。

简单说,死锁问题是由两个或以上的线程并行执行时,因为争抢资源而造成互相等待造成的

3、死锁的四要素

只要同时满足这四个条件,就肯定会死锁:

  • 互斥条件
  • 持有并等待条件
  • 不可剥夺条件
  • 环路等待条件

4、避免死锁的三种思路

避免死锁,只需要破坏掉四大条件的其中一个即可。

互斥条件没办法破坏,本来锁的目的就是互斥

所以只需要破坏以下三项中的其中一项即可:

  • 破坏持有并等待条件:一个线程必须一次性申请所有的锁,不能单独持有某一个锁
  • 破坏不可剥夺条件:一个线程获取不到锁时,就先主动释放持有的所有锁
  • 破坏环路等待条件:规定各个线程获取锁的顺序

注意:三种思路,但是在具体的场景中,每种做法的开销都是不同的,需要找到开销最低的方式。

一般来说:

  • 破坏持有并等待需要死循环检查条件,而且锁的粒度也很大,一般不去使用。
  • 破坏环路等待只需要规定加锁顺序,效率较高

5、避免死锁的三种实现方式

1、破坏持有并等待条件

一次性申请所有锁这个动作属于临界区,应该抽取出一个类来管理,让它作为锁,向外提供两个同步方法:同时获取所有锁、同时释放所有锁。

由于它要作为锁,所以这个对象必须是单例的。

在尝试申请所有资源时,使用while()死循环,注意要加上超时判断。

class Allocator {
    // 维护一个资源列表
    private List<Object> als = new ArrayList<>();

    // 一次性申请所有资源
    synchronized boolean apply(Object from, Object to){
        if(als.contains(from) || als.contains(to)){
            return false;  
        } else {
            als.add(from);
            als.add(to);  
        }
        return true;
    }

    // 归还资源
    synchronized void free(Object from, Object to){
        als.remove(from);
        als.remove(to);
    }
}

class Account {
    // actr应该为单例
    private Allocator actr;
    private int balance;
    // 转账
    void transfer(Account target, int amt){
        // 一次性申请转出账户和转入账户,直到成功
        while(!actr.apply(this, target))
            try{
                // 锁定转出账户
                synchronized(this){              
                    // 锁定转入账户
                    synchronized(target){
                        if (this.balance > amt){
                            this.balance -= amt;
                            target.balance += amt;
                        }
                    }
                }
            } finally {
                actr.free(this, target)
            }
    } 
}

此处while()与synchronized锁粗粒度的区别

在申请“锁”时,两种方式都是串行的。

但是,while()在通过单例对象获取到全部资源后,只需要申请所需的资源,不会影响其他无关的同类操作,可以并行执行。

而synchronized锁粗粒度,在执行时也只能串行执行。

2、破坏不可抢占条件

线程在获取不到锁时,释放手头持有的锁。这一点Java在语言层面是做不到的,不过Java在SDK层面实现了。

因为 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了。线程进入阻塞状态后啥都干不了,也释放不了线程已经占有的资源。

java.util.concurrent 包下的 Lock 是可以轻松解决这个问题的。

它提供了一个方法:tryLock(long, TimeUnit),在一段时间内尝试获取锁,如果最终没有获取到,就执行释放锁的逻辑。

3、破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源,这样就能避免多个线程交叉加锁的情况。

比如转账,就在代码中写死,按照账户id从小到大依次加锁,就不会有问题。

2、活锁

1、什么是活锁

活锁是指,线程没有发生阻塞,但依然执行不下去的情况。

2、活锁的例子

如果两个线程互相改变对方的结束条件,就可能导致双方谁也无法结束。

比如这个程序:

public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

相当于一个抽水一个注水,水池永远不会空或者满

3、死锁与活锁的区别

  • 死锁:两个线程在执行过程中阻塞住了
  • 活锁:两个线程一直没有阻塞,但都无法停止运行

4、活锁的解决方案

增加随机的睡眠时间,将这样的两个线程错开执行,只要一方先运行完了,那么另一方也就能运行完

3、饥饿

1、什么是线程饥饿

饥饿指的是线程因无法访问所需资源而无法执行下去的情况:

  • 在CPU繁忙时,如果一个线程优先级太低,就有可能遇到一直得不到执行
  • 持有锁的线程,如果执行的时间过长,会导致其他阻塞的线程一直获取不到锁

2、线程饥饿的解决方案

有三种方案:

  • 保证资源充足
  • 公平地分配资源,如果有需求可以使用公平锁,不过效率较低,很少使用。
  • 避免持有锁的线程长时间执行

Java中的死锁活锁是多线程并发编程中的常见问题,这些问题可能会导致线程无法向前执行或陷入死循环。下面分别介绍Java死锁活锁的概念和案例。 1. 死锁 死锁是指两个或多个线程相互等待对方释放锁,从而导致所有线程都无法向前执行的状态。简单来说,当两个或多个线程都占有某些资源并且又想要获取对方占有的资源时,就会发生死锁。 下面是一个Java中的死锁例子: ```java public class DeadlockExample { private Object lock1 = new Object(); private Object lock2 = new Object(); public void method1() { synchronized (lock1) { System.out.println("Method 1: Holding lock 1..."); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("Method 1: Holding lock 1 and lock 2..."); } } } public void method2() { synchronized (lock2) { System.out.println("Method 2: Holding lock 2..."); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("Method 2: Holding lock 1 and lock 2..."); } } } public static void main(String[] args) { DeadlockExample example = new DeadlockExample(); Thread t1 = new Thread(() -> example.method1()); Thread t2 = new Thread(() -> example.method2()); t1.start(); t2.start(); } } ``` 在这个例子中,两个线程t1和t2分别调用了method1和method2方法,这两个方法都需要获取lock1和lock2两个锁才能继续执行。由于t1占用了lock1并等待lock2,而t2占用了lock2并等待lock1,因此两个线程都无法释放自己占用的锁,从而陷入死锁状态。 2. 活锁 活锁是指线程们都在运行并尝试执行任务,但是由于某些条件始终无法满足,导致线程们一直在重试,但是最终无法完成任务。这种情况下,线程们看起来像是在不断地活动,但是实际上却没有任何进展。 下面是一个Java中的活锁例子: ```java public class LiveLockExample { private boolean isPolite; public LiveLockExample(boolean isPolite) { this.isPolite = isPolite; } public void bow(LiveLockExample partner) { while (true) { if (isPolite) { System.out.println("I'm polite, I'll bow first"); partner.bowBack(this); isPolite = false; break;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值