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

简介: 通过java模拟一个简单的银行存取款模型,阐述对线程同步的理解

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

场景模拟

  接下来使用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());
    }
}

运行后输出如下结果:
result1

  现在大致的看一下,初始余额为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);
    }
}

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

  现在,我们发现最终余额变成了50,这很显然是个完全不符合预期的错误结果。那么,如何来解释这个现象呢?
lock1
  从上图可以看到,出现数据不一致的原因在于多个线程并发访问了同一个对象,破坏了不可分割的操作,这里的这个共同访问对象就是余额。其实我们所谓预期的‘正确’结果,就是希望先进行存款,然后再进行取款,或者反之。

原子操作与锁

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

下面我们通过锁来实现线程同步访问控制,修改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);
    }
}

运行结果如下:
result3

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

目录
相关文章
|
6天前
|
存储 缓存 关系型数据库
MySQL底层概述—3.InnoDB线程模型
InnoDB存储引擎采用多线程模型,包含多个后台线程以处理不同任务。主要线程包括:IO Thread负责读写数据页和日志;Purge Thread回收已提交事务的undo日志;Page Cleaner Thread刷新脏页并清理redo日志;Master Thread调度其他线程,定时刷新脏页、回收undo日志、写入redo日志和合并写缓冲。各线程协同工作,确保数据一致性和高效性能。
MySQL底层概述—3.InnoDB线程模型
|
6月前
|
编解码 网络协议 API
Netty运行原理问题之Netty的主次Reactor多线程模型工作的问题如何解决
Netty运行原理问题之Netty的主次Reactor多线程模型工作的问题如何解决
|
4月前
|
并行计算 JavaScript 前端开发
单线程模型
【10月更文挑战第15天】
|
4月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
44 1
|
5月前
|
消息中间件 存储 NoSQL
剖析 Redis List 消息队列的三种消费线程模型
Redis 列表(List)是一种简单的字符串列表,它的底层实现是一个双向链表。 生产环境,很多公司都将 Redis 列表应用于轻量级消息队列 。这篇文章,我们聊聊如何使用 List 命令实现消息队列的功能以及剖析消费者线程模型 。
135 20
剖析 Redis List 消息队列的三种消费线程模型
|
4月前
|
NoSQL Redis 数据库
Redis单线程模型 redis 为什么是单线程?为什么 redis 单线程效率还能那么高,速度还能特别快
本文解释了Redis为什么采用单线程模型,以及为什么Redis单线程模型的效率和速度依然可以非常高,主要原因包括Redis操作主要访问内存、核心操作简单、单线程避免了线程竞争开销,以及使用了IO多路复用机制epoll。
80 0
Redis单线程模型 redis 为什么是单线程?为什么 redis 单线程效率还能那么高,速度还能特别快
|
4月前
|
安全 调度 C#
STA模型、同步上下文和多线程、异步调度
【10月更文挑战第19天】本文介绍了 STA 模型、同步上下文和多线程、异步调度的概念及其优缺点。STA 模型适用于单线程环境,确保资源访问的顺序性;同步上下文和多线程提高了程序的并发性和响应性,但增加了复杂性;异步调度提升了程序的响应性和资源利用率,但也带来了编程复杂性和错误处理的挑战。选择合适的模型需根据具体应用场景和需求进行权衡。
|
4月前
|
消息中间件 NoSQL 关系型数据库
【多线程-从零开始-捌】阻塞队列,消费者生产者模型
【多线程-从零开始-捌】阻塞队列,消费者生产者模型
61 0
|
7月前
|
缓存 编译器 Go
开发与运维线程问题之Go语言的goroutine基于线程模型实现如何解决
开发与运维线程问题之Go语言的goroutine基于线程模型实现如何解决
70 3
|
7月前
|
算法 调度 人工智能
人工智能线程问题之无锁化编程如何解决
人工智能线程问题之无锁化编程如何解决
70 2

热门文章

最新文章