一把互斥锁保护多个资源

  • 前言
  • 保护没有关联关系的多个资源
  • 保护有关联关系的多个资源
  • 使用锁的正确姿势
  • 总结

前言

受保护资源和锁之间合理的关联关系应该是 N:1 的关系,可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源

当要保护多个资源时,首先要区分这些资源是否存在关联关系。

保护没有关联关系的多个资源

在现实世界里,球场的座位和电影院的座位就是没有关联关系的,球赛有球赛的门票,电影院有电影院的门票,各自管理各自的。

对应到编程领域,例如,银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,可以为账户余额和账户密码分配不同的锁来解决并发问题。

相关的示例代码如下,账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的。

class Account {// 锁:保护账户余额private final Object balLock = new Object();// 账户余额  private Integer balance;// 锁:保护账户密码private final Object pwLock = new Object();// 账户密码private String password;// 取款void withdraw(Integer amt) {synchronized(balLock) {if (this.balance > amt){this.balance -= amt;}}} // 查看余额Integer getBalance() {synchronized(balLock) {return balance;}}// 更改密码void updatePassword(String pw){synchronized(pwLock) {this.password = pw;}} // 查看密码String getPassword() {synchronized(pwLock) {return password;}}
}

当然也可以用一把互斥锁来保护多个资源,例如可以用 this 这一把锁来管理账户类里所有的资源:账户余额和用户密码。具体实现很简单,示例程序中所有的方法都增加同步关键字 synchronized 就可以了。

但是用一把锁有个问题,就是性能太差,会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的。而用两把锁,取款和修改密码是可以并行的。用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁还有个名字,叫细粒度锁

保护有关联关系的多个资源

如果多个资源是有关联关系的。例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。

声明账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢?

class Account {private int balance;// 转账void transfer( Account target, int amt){if (this.balance > amt) {this.balance -= amt;target.balance += amt;}}
}

直觉方案:用户 synchronized 关键字修饰一下 transfer() 方法就可以了。

class Account {private int balance;// 转账synchronized void transfer(Account target, int amt){if (this.balance > amt) {this.balance -= amt;target.balance += amt;}}
}

在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁 this,符合多个资源可以用一把锁来保护,可惜,这个方案仅仅是看似正确。

问题就出在:this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。

具体分析一下,假设有 A、B、C 三个账户,余额都是 200 元,用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,期望的结果是账户 A 的余额是 100 元,账户 B 的余额是 200 元, 账户 C 的余额是 300 元。

假设线程 1 执行账户 A 转账户 B 的操作,线程 2 执行账户 B 转账户 C 的操作。这两个线程分别在两颗 CPU 上同时执行,期望它们是互斥的,但实际上并不是。因为线程 1 锁定的是账户 A 的实例(A.this),而线程 2 锁定的是账户 B 的实例(B.this),所以这两个线程可以同时进入临界区 transfer()。同时进入临界区的结果是:线程 1 和线程 2 都会读到账户 B 的余额为 200,导致最终账户 B 的余额可能是 300(线程 1 后于线程 2 写 B.balance,线程 2 写的 B.balance 值被线程 1 覆盖),可能是 100(线程 1 先于线程 2 写 B.balance,线程 1 写的 B.balance 值被线程 2 覆盖),就是不可能是 200。

使用锁的正确姿势

用同一把锁来保护多个资源,也就是现实世界的“包场”,在编程领域“包场”只要锁能覆盖所有受保护资源就可以了

在上面的例子中,this 是对象级别的锁,所以 A 对象和 B 对象都有自己的锁。如果让 A 对象和 B 对象共享一把锁,可以让所有对象都持有一个唯一性的对象,这个对象在创建 Account 时传入。示例代码如下,把 Account 默认构造函数变为 private,同时增加一个带 Object lock 参数的构造函数,创建 Account 对象时,传入相同的 lock,这样所有的 Account 对象都会共享这个 lock 了。

class Account {private Object lock;private int balance;private Account();// 创建 Account 时传入同一个 lock 对象public Account(Object lock) {this.lock = lock;} // 转账void transfer(Account target, int amt){// 此处检查所有对象共享的锁synchronized(lock) {if (this.balance > amt) {this.balance -= amt;target.balance += amt;}}}
}

这个办法确实能解决问题,但是有点小瑕疵,它要求在创建 Account 对象的时候必须传入同一个对象,如果创建 Account 对象时,传入的 lock 不是同一个对象,会出现锁自家门来保护他家资产的荒唐事。在真实的项目场景中,创建 Account 对象的代码很可能分散在多个工程中,传入共享的 lock 真的很难。

所以,上面的方案缺乏实践的可行性,需要更好的方案。就是用 Account.class 作为共享的锁。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以不用担心它的唯一性。使用 Account.class 作为共享的锁,就无需在创建 Account 对象时传入了,代码更简单。

class Account {private int balance;// 转账void transfer(Account target, int amt){synchronized(Account.class) {if (this.balance > amt) {this.balance -= amt;target.balance += amt;}}}
}

下面这幅图很直观地展示了如何使用共享的锁 Account.class 来保护不同对象的临界区的。

总结

一把锁如何保护多个资源的关键是要分析多个资源之间的关系。

  • 如果资源之间没有关系,很好处理,每个资源一把锁就可以了。
  • 如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。除此之外,还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁,这个过程可以类比一下门票管理。

关联关系如果用更具体、更专业的语言来描述的话,其实是一种“原子性”特征:主要是面向 CPU 指令的,转账操作的原子性则是属于是面向高级语言的,不过它们本质上是一样的。

“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A 减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见。

一把互斥锁保护多个资源相关推荐

  1. 互斥锁必须用同一个吗_04 | 互斥锁(下):如何用一把锁保护多个资源?

    点击上方蓝字关注我们 在上一篇文章中,我们提到受保护资源和锁之间合理的关联关系应该是 N:1 的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源,并且结合文中示例,我们也重点强 ...

  2. java的尝试性问题_Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和有序性的问题,那么还有一个原子性问题咱们还没解决.在第一篇文章01并发编程的Bug源头当中,讲到了把一个或者多 ...

  3. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等...

    http://blog.51cto.com/13919357/2339446 Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类.介绍的内容 ...

  4. 【Linux C 多线程编程】互斥锁与条件变量

    一.互斥锁 互斥量从本质上说就是一把锁, 提供对共享资源的保护访问. 1) 初始化: 在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化: 对于静态 ...

  5. 10月13日学习内容整理:线程,创建线程(threading模块),守护线程,GIL(全局解释器互斥锁)...

    一.线程 1.概念:一条流水线的工作过程 2.和进程的区别和关系 (1)关系 >进程是资源单位,线程是执行单位,cpu真正执行的是线程 >一个进程至少有一个线程 >多线程针对的是一个 ...

  6. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

    Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类.介绍的内容如下: 公平锁 / 非公平锁 可重入锁 / 不可重入锁 独享锁 / 共享锁 互 ...

  7. l2tp连接尝试失败 因为安全层在初始化_线程安全互斥锁

    1认知 线程安全机制: 由于线程它是共享进程里面所有的资源,当然这里面就会包括虚拟内存里面所有的东西(包含全局变量,堆内存,映射的内存及程序段落等),它也继承了进程当中所有的资源(文件描述符,信号资源 ...

  8. 互斥锁深度理解与使用

    大家好,我是易安! 我们知道一个或者多个操作在CPU执行的过程中不被中断的特性,称为"原子性".理解这个特性有助于你分析并发编程Bug出现的原因,例如利用它可以分析出long型变量 ...

  9. 线程同步之互斥量(互斥锁)

    1 同步的概念 所谓同步, 即同时起步,协调一致.不同的对象, 对"同步" 的理解方式略有不同. 如,设备同步,是指在两个设备之间规定一个共同的时间参考: 数据库同步, 是指让两个 ...

  10. 同步和互斥的POSXI支持(互斥锁,条件变量,自旋锁)

    同步和互斥在多线程和多进程编程中是一个基本的需求,互相协作的多个进程和线程往往需要某种方式的同步和互斥.POSIX定义了一系列同步对象用于同步和互斥. 同步对象是内存中的变量属于进程中的资源,可以按照 ...

最新文章

  1. 四川大学计算机学院2020推免公示,四川大学2020年推免生录取情况分析
  2. Eclipse搭建Mybatis框架
  3. funny alphabet
  4. linux命令--VI命令详解(一)
  5. zigbee cc2530地址空间 layout 和flash操作
  6. Action framework BAdI Definition TRIGGER_EXECUTED
  7. 被一个熟悉的面试题问懵了:StringBuilder 为什么线程不安全?
  8. STM32——直流电机PI调速
  9. 虚拟局域网——vlan (讲解+配置)
  10. Codeforces 1006 F - Xor-Paths
  11. Java并发编程之volatile关键字解析
  12. 数据时代,嵌入式工程师必须知道的八大加密算法
  13. 如何恢复误删的注册表
  14. IDL---批量波段合成(只要点击运行,自动化处理,解放生产力)
  15. Mysql语句(二)
  16. android popup
  17. uni-app 页面组件生命周期
  18. Octree(八叉树)
  19. mac安装oh-my-zsh出现command not found: npm问题解决
  20. 史上最强红利指数——标普A股红利机会指数全解析

热门文章

  1. 性能指标TP99介绍
  2. CentOS7安装Pure-ftpd
  3. 经纬度转WGS84坐标
  4. 打造Win10+WSL开发环境(2)
  5. Windows 7/10下安装Ubuntu 16.04双系统
  6. 前端开发: 微信小程序 (文字,链接)生成二维码
  7. matlab将水印图像嵌入图像,改进的图像自嵌入水印算法及其MATLAB实现
  8. mac更新系统版本后的安装包路径
  9. Odoo 继承对象增加属性,不显示protal o_affix_enabled 内容
  10. CentOS 7 虚拟机网卡失效问题:ens33:<NO-CARRIER,BROADCAST,MULTICAST,UP>mtu 1508 gdisc pf ifo_fast state DOWN