Java面向对象程序设计|模拟银行存取款

Java面向对象程序设计|模拟银行存取款
文章图片
图5.5展示了不同线程对同一数据进行存取的两种情况 。 对情况a) , 因只存在读操作 , 类似多人看报 , 各线程获取的数据是确定的;对情况b) , 因有写操作 , 当线程以不同顺序执行时 , D读取到的值、x的最终值是不确定的 。 如以D-E-F次序执行 , D读到1 , x值为3;若以F-E-D次序执行 , D读到2 , x值为2 。
Java面向对象程序设计|模拟银行存取款
文章图片
■图5.5多个线程对同一数据读写的两种情况
这种多个线程对同一数据的并发读写(至少有一个线程执行写操作)被称作竞争 。 竞争会导致数据的不确定性 。 这种不确定性 , 有资料将其视为数据访问不安全 。
下面以银行存取钱为例介绍上述不确定性必须要被解决 。 假设银行有账户张三 , 账户余额100元 。 现张三及其儿子同时在不同存取款一体机上对该账户进行操作 。 张三存入200元 , 其儿子取出300元 。 假设使用存取一体机时 , 必须经历{查-改-查}三个步骤 。 为在本地显示余额和修改金额 , 一体机上需要有这两个本地变量:余额和输入的数据 , 见图5.6 。 为方便分析 , 图中对不同位置上的数据和指令做了不同标注 , 用c/q表示输入的存钱/取钱金额;cm/qm、m分别表示本地缓存余额和账户余额;c1、q1等表示不同机器上的动作 。
■图5.6两个线程向同一银行账户同时存取款
根据需求 , 有m=100 , c=200 , q=300 。 假设执行序列为{q1-c1-q2-c2-q3-c3} , 对应的指令序列:qm=m;cm=m;m=qm-q;m=cm+c;qm=m;cm=m;代入数据 , 结果如下:
qm=100;cm=100;m=100-300=-200;m=100+200=300;qm=300;cm=300
换言之 , 账户原有100元 , 存200 , 取300 , 执行完毕后 , 最终余额300 。 银行显然不能容忍 。 可能的执行序列很多 , 这里不再赘述 , 请读者自行分析 。
Java面向对象程序设计|模拟银行存取款】上述现象是{c1-c2-c3}和{q1-q2-q3}以交错方式对同一账户竞争存取所致 。 若二者以“不可分割”的方式(也称独占方式或互斥方式)执行 , 如现存后取 , 或是先取后存 , 则不会出现上述状况 。 Java用synchronized(D){S}框架实现互斥 。 其中对象D称作临界资源 , 代码段S称作临界区 。 该框架表示:S只能以原子(即不可分割)方式访问D 。
01
线程的互斥机制-示例
【例5.5】假设账户张三有余额100元 , 对账户的存取钱过程的动作序列均为“查-改-查” 。 需要存入200 , 取出300 。 借助线程机制 , 模拟对张三的账户同时实施存取 。
目的:掌握互斥机制的实现和应用框架 。
设计:本例主要设计了两个类:账户类Accoun、实施存/取钱的ATM类 。 其中:
Account类有属性:姓名、金额 , 用构造函数对这些属性赋值 , 以及对金额的read/write方法、获取姓名的getName方法 。
ATM类涉及对账户存/取特定金额 , 故有两个属性:账户a、存/取金额atmVal(正数表示存钱、负数表示取钱) 。 为模拟同时执行 , ATM类必须是线程类 , 在run实施存钱或取钱 , 基本流程为“查-改-查” 。
注意:由于指令执行速度非常快 , 即使未用互斥框架 , 线程体运行也可能一次运行完毕(即存钱过程和取钱过程未发生指令交错) 。 这样难以发现设计问题 。 故线程体中加入了一些sleep动作 。 sleep执行时 , 当前线程必须放弃处理机控制权转入休眠 , 线程切换自然就发生了 。 因此加入sleep可模拟线程频繁切换情形(最坏情形) 。 这是调试线程的常用策略 。 另外 , sleep所属线程会放弃cpu , 但不会放弃对资源的占用(即不会打开锁) 。