• 2. Java并发编程-互斥锁、死锁


    上一节讲了引起并发问题原因中的可见性和有序性。 通过利用Java内存模型开发者可以很好的避开上述问题。 本节我们来探索剩下的一个引起并发问题的原因:原子性。
    什么是原子性?

    即一个或多个操作在CPU执行的过程中不被中断的特性。

    互斥

    一段代码同一时刻只有一个线程运行,称为互斥。 当我们能保证对共享变量的写是互斥的,就能保证原子性 了。

    临界区

    将一段需要互斥执行的代码称为临界区。

    Java提供的锁技术:synchronized

    1)当修饰静态方法时,锁定的是当前类Class对象
    2)当修饰非静态方法时,锁定的是当前对象

    死锁

    一组互相竞争资源的线程因相互等待,导致永久阻塞的现象。

    如何解决死锁问题

    一旦发生死锁通常没有好的方法,只能重启应用。 重点是在预防死锁。

    死锁发生的条件:
    1)互斥,共享资源A和B只能被一个线程占用
    2)占有且等待,线程一已经取得了资源A,在等待B资源的时候不会释放资源A
    3)不可抢占,其他线程不能强行抢占线程A占有的资源
    4)循环等待。 如A线程等待B占有的资源,线程B又等待A线程占有的资源

    以上4个条件缺一不可,反过来意味着我们只要破坏其中一条即可解决死锁问题。
    这其中第一条无法破除,因为用锁就是为了互斥性。
    第二条占用且等待,我们可以一次性申请所有的资源
    以转账为示例,将同时申请两个账户的锁操作放到临界区中,保证只能一个线程同时申请到两个锁资源,示例代码如下:

    class Account {
    
        private static final Allocate allocate = new Allocate();
    
        private int balance;
    
        public Account(int balance) {
            this.balance = balance;
        }
    
        void transfer(Account target, int amount) {
            while (allocate.apply(this, target))
                ;
    
            try {
                synchronized (this) {
                    synchronized (target) {
                        if (this.balance >= amount) {
                            this.balance -= amount;
                            target.balance += amount;
                        }
                    }
                }
    
            } finally {
                allocate.free(this, target);
            }
        }
    
    
    }
    
    
    class Allocate {
        private List<Object> locks = new ArrayList<>();
    
        synchronized boolean apply(Object lock1, Object lock2) {
            if (locks.contains(lock1) || locks.contains(lock2)) {
                return false;
            }
    
            locks.add(lock1);
            locks.add(lock2);
    
            return true;
        }
    
        synchronized void free(Object lock1, Object lock2) {
            locks.remove(lock1);
            locks.remove(lock2);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    第三条不可抢占,当占用部分资源的线程在进一步申请其他资源时,如果申请不到,可以主动放弃它已占有的资源
    这一点默认synchronized锁是做不到的。即语言层面无法做到,但SDK可以,java.util.concurrent并发包下提供的Lock类可以解决此类问题。

    第四条循环等待,可以靠按序申请资源来预防。
    这一点比较好理解,即保持不同线程加锁的顺序一致,能避免互相持有对方锁的情况。

    小结

    当使用互斥锁时,要分析多个资源之间的关系,如果没关系,每个资源一把锁即可,如果资源之间有关系,就要选择一个粒度更大的锁,能覆盖所有相关的资源。

    当使用细粒度锁在锁定多个资源时,要注意死锁的问题,能及时识别风险很重要。破除死锁即可用上面列出的三个方法,但使用不同方法时页需要比较优劣,上面转账的示例采用了破坏占用且等待的方法,实际上就不如破除不可抢占和破除循环等待简单高效。

  • 相关阅读:
    Spring Cloud【为什么需要监控系统、Prometheus环境搭建、Grafana环境搭建 、微服务应用接入监控 】(十七)
    山东省技能兴鲁网络安全大赛 web方向
    聚氯乙烯含汞废水处理工艺
    git rebase 使用详解
    算法--选择排序
    基于NIFI工具搭建生产级别的IIOT系统
    工程企业管理软件源码-综合型项目管理软件
    不抖机灵!让工程师来告诉你做芯片是如何烧钱的!
    粘滞位和vim的使用
    IPA模特委员会十月人气评选 石庭语荣获全球人气季军
  • 原文地址:https://blog.csdn.net/qq_25027457/article/details/125599149