初级模拟-synchronized应用场景-并发编程(Java)


现实中有很多多线程应用场景,我们以售票和银行转账为例,来完成场景的简单模拟。

1 售票

  • 应用场景:假定当前山东泰山站有5个售票窗口开放,每个售票窗口有1名售票员,从山东泰安到北京的普快车票还有100张;旅客可能是一个人,也可能是几个人一起,那么买票的时候可能买多张票,这里限定1名旅客最多买5张票。

  • 分析:5个售票窗口(5名售票员)共享这100张车票,我们把售票窗口建模为这100张票的容器。旅客和售票员用多线程简单模拟自动买票和卖票。旅客购买的票数作为参数传入卖票方法。建模如下,

    • 车票:数字代表,作为窗口类的属性
    • 窗口类:
      • 属性:剩余车票数
      • 方法:sell(int amount),售票
    • 售票员,旅客:用多线程简单模拟
  • 初级实现:

    • 窗口类代码1-1如下:

    • public class TicketWindow {
          private int count;
      
          public TicketWindow(int count) {
              this.count = count;
          }
      
          /**
           * 获取剩余票数
           * @return  剩余票数
           */
          public int getCount() {
              return count;
          }
      
          /**
           * 售票
           * @param amount    售票数
           * @return          实际售票数
           */
          public int sell(int amount) {
              if (count >= amount) {
                  count -= amount;
                  return amount;
              } else {
                  return 0;
              }
          }
      }
      
    • 测试类:如何验证多线程环境下的安全性呢?如果剩余票数加上卖出票数等于初始票数,那么可以验证是多线程安全的;否则就是多线程不安全的,代码如下

      import java.util.ArrayList;
      import java.util.List;
      import java.util.Random;
      import java.util.Vector;
      
      /**
       * @author Administrator
       * @version 1.0
       * @description 售票测试
       * @date 2022-10-14 10:30
       */
      @Slf4j(topic = "c.TestSellTicket")
      public class TestSellTicket {
          public static void main(String[] args) throws InterruptedException {
              // 卖票窗口
              TicketWindow ticketWindow = new TicketWindow(100);
              // 售票员线程列表
              int initial = 2000;
              List<Thread>  seller = new ArrayList<>(initial);
              // 每次销售的票数列表
              List<Integer> sellTickets = new Vector<>();
              for (int i = 0; i < initial; i++) {
                  Thread t = new Thread(() -> {
                      int amount = ticketWindow.sell(randomAmount() );
                      try {
                          Thread.sleep(randomAmount() * 100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      sellTickets.add(amount);
                  }, "售票员" + i);
                  seller.add(t);
                  t.start();
              }
              for (Thread thread : seller) {
      //            thread.start();
                  thread.join();
              }
      
              // 通过销售票数和剩余票数检查是否有问题
              int remainders = ticketWindow.getCount();
              log.debug("余票:{}", remainders);
              int sells = sellTickets.stream().mapToInt(i -> i).sum();
              log.debug("卖出票数:{}", sells);
      
          }
      
          /**
           * Random线程安全
           */
          static Random random = new Random();
      
          /**
           * 随机票数1~5
           * @return  票数
           */
          public static int randomAmount() {
              return random.nextInt(5) + 1;
          }
      }
      

分析有哪些共享变量 变量组合 是否是临界区需要保护:

  • 线程run方法涉及3个变量,

    • seller:售票员线程列表,此变量由主线程使用,所以不存在安全性问题
    • sellTickets:类型为Vector为线程安全类
    • ticketWindow:调用了给实例的sell方法,此方法改变状态count的值,没有进行并发访问控制,存在线程安全问题
  • 临界区:很显然,就是TicketWindow的sell方法

  • 要得到预期的结果,需要我们多次运行,一下一下点很麻烦,这里我们通过cmd命令执行脚本的方法来多次执行,脚本如下:

    for /L %n in (1,1,10) do java -cp ".;你自己的maven仓库路径\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;你自己的maven仓库路径\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;你自己的maven仓库路径\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar" 你自己的包路径.TestSellTicket
    
    • jar包路径为引入的日志打印相关类路径,当前执行脚本的路径为target/classes路径,用的Idea
  • 解决方案:根据目前学习的知识,就是在TicketWindow的sell方法上加锁,改造TicketWindow的sell方法,TicketWindow的其他代码不变,代码如下

    /**
         * 售票
         * @param amount    售票数
         * @return          实际售票数
         */
        public synchronized int sell(int amount) {
            if (count >= amount) {
                count -= amount;
                return amount;
            } else {
                return 0;
            }
        }
    

在此测试数据没有问题。

2 转账

  • 应用场景:简单模拟银行转账。

  • 以最简单2个储户之间的转账为例,建模如下:

    • 金额money:数字代表,作为账户类的属性
    • 账户类Account:
      • 属性:money金钱
      • 方法:
        • getMoney:获取账户金额
        • setMoney(int amount):设置账户金额
        • transfer(Account a, int amount):当前账户给账户a转账amount元
    • 多次相互转账:用多线程简单模拟
  • 初级实现,代码如下

    • 账户类:

      public class Account {
          private int money;
      
          public int getMoney() {
              return money;
          }
      
          public void setMoney(int money) {
              this.money = money;
          }
      
          /**
           * 转账
           * @param a         转入账户
           * @param amount    转入金额
           */
          public void transfer(Account a, int amount) {
              // 判断当前金额是否大于等于转账金额
              if (money >= amount) {
                  // 当前账户减去转账金额
                  money -= amount;
                  // 转入账户加上转账金额
                  a.setMoney(a.getMoney() + amount);
              }
          }
      }
      
    • 测试类:

    • import lombok.extern.slf4j.Slf4j;
      import java.util.Random;
      
      /**
       * @author Administrator
       * @version 1.0
       * @description 测试
       * @date 2022-10-15 11:25
       */
      @Slf4j(topic = "c.TestTransfer")
      public class TestTransfer {
          public static void main(String[] args) throws InterruptedException {
              Account a = new Account(1000);
              Account b = new Account(1000);
      
              Thread t1 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      a.transfer(b, randomAmount());
                  }
              });
              Thread t2 = new Thread(() -> {
                  for (int i = 0; i < 100; i++) {
                      b.transfer(a, randomAmount());
                  }
              });
              t1.start();
              t2.start();
              t1.join();
              t2.join();
      
              // 转账200次后的总金额
              log.debug("total:{}", (a.getMoney() + b.getMoney()));
          }
      
          /**
           * Random线程安全
           */
          static Random random = new Random();
      
          /**
           * 随机1~100
           * @return  1~100随机整数
           */
          public static int randomAmount() {
              return random.nextInt(100) + 1;
          }
      }
      
    • 多次测试结果不确定,有时大于2000,有时小于2000;大于2000银行不愿意啊,小于2000储户不愿意,这肯定不行啊。

  • 分析:问题出在transfer转账方法中,那么这里面有哪些共享变量呢?

    • 当前账户的金额:this.money

    • 转入账户的金额:a.money

    • 那么我们直接给transfer方法加synchronized行不行呢;测试并不行,因为这相当于只给当前对象加锁,并没有给转入账户加锁

    • 那么我们能不能先给当前加锁在给转入对象加锁呢?这容易引起死锁问题,后面讲解。

    • 那么以我们目前学习的知识,怎么解决呢?寻找共同点,2个或者多个对象都是同一个类的对象,我们给Account.class对象加锁,代码如下

      /**
           * 转账
           * @param a         转入账户
           * @param amount    转入金额
           */
          public void transfer(SafeAccount a, int amount) {
              synchronized (Account.class) {
                  // 判断当前金额是否大于等于转账金额
                  if (money >= amount) {
                      // 当前账户减去转账金额
                      money -= amount;
                      // 转入账户加上转账金额
                      a.setMoney(a.getMoney() + amount);
                  }
              }
          }
      
  • 思考:一个银行有很多储户,如果给Account.class加锁,虽然保证了线程安全性,但是极大的降低效率,有没有更好的解决方案呢?

3 后记

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent

参考:

[1]黑马程序员.黑马程序员深入学习Java并发编程,JUC并发编程全套教程[CP/OL].2020-01-18/2022-10-02.p71~p74.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gaog2zh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值