• 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类可以解决此类问题。

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

    小结

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

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

  • 相关阅读:
    RHCE8 资料整理(一)
    Ceph文件存储
    【wavesurfer.js实战范例】多区域音频标注(含区域实时切换显示)
    Linux内核源码分析 (B.11) 从内核世界透视 mmap 内存映射的本质(原理篇)
    el-form-item validator 携带额外的参数
    Vue插件
    小程序设计基本微信小程序的校园生活助手系统
    一个去掉PDF背景水印的思路
    Flask 学习-34.restful-full 请求参数自定义参数校验类型 (reqparse.RequestParser() )
    Spring AOP:原理与示例代码
  • 原文地址:https://blog.csdn.net/qq_25027457/article/details/125599149