互斥锁
关于原子性的问题:
- 如果我们能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,都能保证其满足原子性了。
Synchronized
- 是锁的一种实现。synchronized 关键字可以用来修饰方法和代码块。Java编译器自动在代码块前后加锁解锁
- 修饰方法时的锁定对象:
- 当修饰静态方法时,锁定的是当前类的 Class 对象
- 当修饰非静态方法时,锁定的是当前实例对象 this。
- 修饰代码块时:锁住的是代码块中配置的对象
- 锁和受保护资源的关系
- 受保护资源和锁之间的关联关系是 N:1 的关系,可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源。并且锁需要覆盖所有受保护资源
- 使用 Object(当前对象).class 作为共享的锁,就无需在创建对象时传入了。
- Object(当前对象).class 是所有 Object(当前对象)对象共享的,而且这个对象是 Java 虚拟机在加载 Object(当前对象)类的时候创建的,所以它是唯一的。
锁的粒度
- 细粒度锁:用不同的锁对受保护资源进行精细化管理,能够提升性能
- 如果资源之间存在关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源
原子性问题的本质:并不是不可分割,不可分割只存在于外在表现,本质是多个资源间有一致性的要求,操作的中间状态对外不可见;解决原子性问题,是要保证中间状态对外不可见
关于死锁
- 使用细粒度锁可能会导致死锁
- 死锁:一组互相竞争资源的线程因互相等待,导致永久阻塞的现象
- 如何产生死锁
- 互斥,共享资源X和Y只能被一个线程占用
- 占有且等待,线程T1已经取得了共享资源X,在等待共享资源Y的时候,不释放共享资源X
- 不可抢占,其他线程不能强行抢占线程T1占有的资源
- 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源
- 避免死锁(只要我们能够破坏上条的一个条件就可以成功避免)
- 互斥无法避免,因为锁用的就是互斥锁
- 对于占有且等待,可以一次性申请全部资源
- 对于不可抢占,占有一部分资源的线程想继续申请其他资源时,如果申请不到,可以主动释放他占有的资源,以退为进(如果申请不到是因为其他线程占用并且等待当前线程所占有的资源时,释放掉资源把他要的给他,等他执行完了咱们在执行)
- 对于循环等待,可以靠按序申请资源来预防。所谓按需申请是指资源是存在线性顺序的,申请的时候可以先申请资源序号小的,在申请大的,这样线性化后就不会循环等待了
案例:
破坏占用且等待条件
- 账户 Account 类里面持有一个 Allocator 的单例(必须是单例,只能由一个人来分配资源)。当账户 Account 在执行转账操作的时候,首先向 Allocator 同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,我们需通知 Allocator 同时释放转出账户和转入账户这两个资源。
- “同时申请”这个操作是一个临界区,我们用 Allocator这个类来管理这个临界区。它有两个重要功能:同时申请资源 apply() 和同时释放资源 free()。
破坏不可抢占条件
- 主动释放本线程占有的资源,这一点synchronized无法做到。
- 原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。
- 不过在 SDK 层面还是解决了的,java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。(提供tryLock(long, TimeUnit) 方法,在一段时间尝试获取锁。)
破坏循环等待条件
- 破坏这个条件,需要对资源进行排序,然后按序申请资源。我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。比如对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。