银行存取款模型的线程同步问题

  关于线程同步,网上也有很多资料,不过不同的人理解也不大一样,最近在研究这个问题的时候回想起大学课本上的一个经典模型,即银行存取款模型,通过这个模型,我个人感觉解释起来还是比较清楚的。本文结合自己的思考对该模型进行一个简单的模拟,阐述一下我对线程同步的理解。

场景模拟

  接下来使用java对该问题进行模拟。在研究这个问题时会忽略掉现实系统中的很多其他属性,通过一个最简单的余额问题来看线程同步,这里首先创建三个类。

1.卡类,同时卡类提供三个方法,获取余额、存款以及取款。

public class Card {

    /余额初始化/
    private double balance;
    public Card(double balance){
        this.balance = balance;
    }

    /获取余额方法/
    public double Get_balance(){
        return this.balance;
    }

    /存款方法/
    public void deposit(double count) throws InterruptedException{
        System.out.println("存钱线程:存入金额=" + count);
        double now = balance + count;
        balance = now;
        System.out.println("存钱线程:当前金额=" + balance);
    }

    /取款方法/
    public void withdraw(double count) throws InterruptedException{
        System.out.println("取钱线程:取出金额=" + count);
        double now = balance - count;
        balance = now;
        System.out.println("取钱线程:当前金额=" + balance);
    }
}

然后是两个线程类,用于模拟并发操作所引入的余额问题。

2.存款线程类,存入金额100。

public class DepositThread extends Thread{
    private Card card;
    public DepositThread(Card card){
        this.card = card;
    }
    @Override
    public void run(){
        try {
            card.deposit(100);
        }
        catch(Exception e){System.out.println(e.toString());}
    }
}

3.取款线程类,取出金额50。

public class WithdrawThread extends Thread{
    private Card card;
    public WithdrawThread(Card card){
        this.card = card;
    }
    @Override
    public void run(){
        try {
            card.withdraw(50);
        }
        catch(Exception e){
            System.out.println(e.toString());
        }
    }
}

  现在先进行一个测试,让存款线程先进行存钱操作,然后取款线程进行取款,最后验证余额与逻辑是否符合。

测试代码如下:

public class CardTest{
    public static void main(String[] args) throws InterruptedException{
        Card card = new Card(100);
        System.out.println("操作前余额:" + card.Get_balance());
        DepositThread depositThread = new DepositThread(card);
        WithdrawThread withdrawThread = new WithdrawThread(card);
        depositThread.start();
        withdrawThread.start();
        Thread.sleep(2000);
        System.out.println("最终余额:" + card.Get_balance());
    }
}

运行后输出如下结果:

  现在大致的看一下,初始余额为100,然后存款线程存入100,接下来取款线程取走50,那么最后余额为150。这么看来,貌似没问题?

数据不一致问题

  事实上,存取款过程是需要消耗时间的,只要一个线程在操作余额期间受到其他线程的干扰,就可能出现数据不一致问题。这里我们修改存取款方法的代码如下。

存款方法:

    public void deposit(double count) throws InterruptedException{
        System.out.println("存钱线程:存入金额=" + count);
        double now = balance + count;
        Thread.sleep(100);  //存钱的操作用时0.1s
        balance = now;
        System.out.println("存钱线程:当前金额=" + balance);
    }

取款方法:

    public void withdraw(double count) throws InterruptedException{
        System.out.println("取钱线程:取出金额=" + count);
        double now = balance - count;
        Thread.sleep(200);  //取钱的操作用时0.2s
        balance = now;
        System.out.println("取钱线程:当前金额=" + balance);
    }
}

然后再运行一遍测试程序:

  现在,我们发现最终余额变成了50,这很显然是个完全不符合预期的错误结果。那么,如何来解释这个现象呢?

  从上图可以看到,出现数据不一致的原因在于多个线程并发访问了同一个对象,破坏了不可分割的操作,这里的这个共同访问对象就是余额。其实我们所谓预期的‘正确’结果,就是希望先进行存款,然后再进行取款,或者反之。

原子操作与锁

  上面提到‘不可分割的操作’,这种操作就是原子操作。是因为实际上多线程编程的情境下,很多敏感数据不允许被同时访问,因此对于这种针对敏感数据的操作,需要进行线程访问的协调与控制,这就是所谓的线程同步(协同步调)访问技术。线程同步控制的结果,就是把每次对敏感数据的操作变成原子操作,从而让执行顺序按照我们预期的过程进行。
  上述情境下,存款与取款应当是两个原子操作,我们必须保证先进行且完成存款操作再进行取款操作,才能保证最终数据的一致性,才能得到我们认为是‘正确’的结果。

下面我们通过锁来实现线程同步访问控制,修改Card类的代码如下。

public class Card {

    private double balance;
    private Object lock = new Object(); //锁

...省略其它代码

    /存款/
    public void deposit(double count) throws InterruptedException{
        System.out.println("存钱线程:存入金额=" + count);
        synchronized (lock) {
            double now = balance + count;
            Thread.sleep(100);//存钱的操作用时0.1s
            balance = now;
        }
        System.out.println("存钱线程:当前金额=" + balance);
    }

    /取款/
    public void withdraw(double count) throws InterruptedException{
        System.out.println("取钱线程:取出金额=" + count);
        synchronized (lock) {
            double now = balance - count;
            Thread.sleep(200);//取钱的操作用时0.2s
            balance = now;
        }
        System.out.println("取钱线程:当前金额=" + balance);
    }
}

运行结果如下:

  这段代码中,通过synchronized 关键字保证lock对象只能同时被一个线程访问,要想操作余额,那么必须先获取lock对象的访问许可,因此就保证了余额不会被多个线程同时修改,而最终的结果也完全符合我们的预期。这个lock对象就可以形象的理解成锁,整个执行过程大致如下图所示,

时间: 2024-11-03 22:09:49

银行存取款模型的线程同步问题的相关文章

Java线程同步实例分析_java

本文实例讲述了Java线程同步的用法.分享给大家供大家参考.具体分析如下: 多线程的使用为我们的程序提供了众多的方便,同时它也给我们带来了以往没有考虑过的麻烦.当我们使用多线程处理共享资源时意外将会发生:比如我们一起外出就餐,每个人都是一个线程,餐桌上的食物则是共享资源,当我看到红烧鸡腿上桌后立即拿起筷子直奔目标,眼看着就得手的时候,突然---鸡腿消失了,一个距离盘子更近的线程正在得意地啃着. 为了避免上述问题的发生,Java为我们提供了"synchronized(同步化)修饰符"来避

银行取款[多线程]{未进行线程同步}(junit不适合多线程并发单元测试)

        由于计算机多任务.多进程.多线程的支持,使得计算机资源的服务效率提高,服务器对请求的也使用线程来相应,所有,代码中涉及到同时对共享数据的操作,将在 多线程环境中操作数据,导致数据安全问题.      经典例子:老婆(朱丽叶)老公(罗密欧),使用银行卡和存折,或者网银等,同时对同一账户操作的安全问题.      如果要保证多线程下数据安全,就要实现线程同步(例如:一间小厕所,就得有一个锁,保证同一时间为一个人服务).其他文章讲: 此处用多线程实现,同时取款的模拟实现,未进行线程同步

银行取款[多线程]{使用重入锁Lock接口ReentrantLock锁确保线程同步}

经典例子:老婆(朱丽叶)老公(罗密欧),使用银行卡和存折,或者网银等,同时对同一账户操作的安全问题.  此处用多线程实现,同时取款的模拟实现,使用使用Lock接口ReentrantLock锁确保线程同步,查看取款安全隐患问题,代码如下: -----------------------------------------------------------------------------------------------------------------------------------

银行取款[多线程]{使用volatile修饰共享变量,但此场景并不保证线程同步}

经典例子:老婆(朱丽叶)老公(罗密欧),使用银行卡和存折,或者网银等,同时对同一账户操作的安全问题. 此处用多线程实现,同时取款的模拟实现,使用volatile修饰共享变量,但此场景并不保证线程同步,查看取款安全隐患问题,代码如下: 我学习地址(Thanks for auther): Java 理论与实践: 正确使用 Volatile 变量 java中volatile关键字的含义 ----------------------------------------------------------

银行取款[多线程]{使用ThreadLocal管理共享变量,但此场景并不保证线程同步}

经典例子:老婆(朱丽叶)老公(罗密欧),使用银行卡和存折,或者网银等,同时对同一账户操作的安全问题. 此处用多线程实现,同时取款的模拟实现,使用ThreadLocal管理共享变量,但此场景并不保证线程同步,查看取款安全隐患问题,代码如下: ----------------------------------------------------------------------------------------------------------------------------------

银行取款[多线程]{使用同步方法确保线程同步}

  经典例子:老婆(朱丽叶)老公(罗密欧),使用银行卡和存折,或者网银等,同时对同一账户操作的安全问题.  此处用多线程实现,同时取款的模拟实现,使用同步方法确保线程同步,查看取款安全隐患问题,代码如下: -------------------------------------------------------------------------------------------------------------------------------------------  * 线程同步

银行取款[多线程]{使用同步代码块确保线程同步}

经典例子:老婆(朱丽叶)老公(罗密欧),使用银行卡和存折,或者网银等,同时对同一账户操作的安全问题. 此处用多线程实现,同时取款的模拟实现,使用同步代码块确保线程同步,查看取款安全隐患问题,代码如下: --------------------------------------------------------------------------------------------------------------------------------------  * 线程同步 :使用同步块

java内存模型与线程(转) good

java内存模型与线程 参考 http://baike.baidu.com/view/8657411.htm  http://developer.51cto.com/art/201309/410971_all.htm http://www.cnblogs.com/skywang12345/p/3447546.html 计算机的CPU计算能力超强,其计算速度与 内存等存储 和通讯子系统的速度相比快了几个数量级, 数据加载到内存中后,cpu处理器运算处理时,大部分时间花在等待获取去获取磁盘IO.网络

JAVA线程同步实例教程_java

线程是Java程序设计里非常重要的概念,本文就以实例形式对此加以详细解读.具体分析如下: 首先,线程加锁有什么用处呢?举个例子:比如你现在有30000块大洋在银行存着,现在你到银行取钱,当你输入密码完成后,已经输入取款金额,比如你输入的是20000,就是在银行给你拿钱这个时刻,你老婆也去银行取这笔钱,你老婆同样取20000,因为此时你的账上仍然是30000,所以银行同样的操作在你老婆那端又进行了一遍,这样当你们两个完成各自操作后,银行记录的你账上还应该有10000块存款,这样是不是很爽.解决这个