从0开始的多线程打水生活前言

git代码库在此,按照代码库的ReadME.md来帮我复刻一下工程师的防止我张三薅羊毛的操作吧!

【ps:建议看完本文后去防止张三薅羊毛,更容易】

前言

这个问题对于各位大佬们应该是很简单的,所以这个题目只是练习 + 现实拓展,开心就好 (^▽^)。

以下内容是我本人自己原创写的,打工人何苦为难打工人,希望大家转载的时候可以标注原文链接,感谢!

入冬以来,帝都的风是愈发寒冷了,这一天,我同往常一样,穿着厚厚的花裤子和花衣服,手里提着水桶,像京城上了年纪的大爷一样【ps:大爷可比我还注意形象穿着】。至于目的地,那自然是小区的打水机了,那打水机的水,啧,怎么说呢,甘甜,清爽,顺滑。

眼看我手里两桶水桶,饮水机两个接水口,我想着,能不能同时打水呢?

机器.jpg
说干就干

先打一个,记住此时我的钱是7.5
Inked一个打水.jpg

再打第二个,右边正在打水的钱在减少,但是左边还是刚开始的7.5

两个打水处理过.jpg

我自然发现了,应该是等我按停止按钮,钱才会扣掉。

然后我就回去了。

作为薅羊毛界的大佬,经过我的苦思冥想,我得出了一个秘密:如果我只有1桶水的钱,用这种方式打两桶水,那么是不是就薅到羊毛了呢?
也就是说,我先打右边的,钱显示1元,与此同时,左边的也开始打水,钱也显示1元,那么就可以用1块钱喝两桶水了。

当我洋洋自得,打完水按完停止按钮后,看向了手机显示的金额,我愣了,-1元

原来,我的如意算盘,早就被人发现了。

这次,还是这位工程师更胜一筹。

回去之后,菜鸡的我苦思冥想,我能不能也做一个这个呢?这个是怎么实现的呢?

于是,就有了这篇博客


故事讲完了,欲知后事如何,请听下回分解!

好了,不扯淡了,下面是正经的。

练习点

  1. 多线程
  2. 【会设计模式可能会简单那么一点】

详细介绍

  1. 打水机器 Machine
  2. 每一台机器都有两个口可以打水,方便起见,我叫他 Cell 【单元格】。(要是有更好的叫法请在评论区指出)
  3. 简化图
        Machine
          ↓
    出水口A   出水口B
    start     start
    stop      stop

问题

正常情况下,用户按开始按钮开始打水,按结束按钮停止打水,扣钱

【为了模拟这种开始打水和结束打水的情况,我提供了简化的方式,也就是CellRunner.run 可以指定打水时间,比如打水1秒,10秒等,
就不需要模拟用户操作了】

  1. 保证单个用户打水可以正常扣钱
  2. 保证 张三李四 同时使用打水机器时,都可以正常打水,正常扣钱,不会影响
  3. 保证 张三 同时使用一台机器的两个打水口 想薅羊毛时,识破他的想法,正常打水和扣钱
  4. 保证张三 同时使用2台机器的所有打水口,即同时使用4个打水口,还可以正常打水和扣钱。【ps:为了薅羊毛,张三也是拼了!】

完成方式

  1. git代码库在此,按照代码库的ReadME.md来帮我复刻一下工程师的防止我张三薅羊毛的操作吧!
  2. 大佬可以自己编写代码,按照自己的代码格式和组织结构,只需要实现和我文章描述的效果即可

完成输出

readme文件有3个难度,此处提供一下本文介绍的避免张三薅羊毛的输出结果

tom: 20元

Cell:B : 开始取水
单元格A : 开始取水

Cell:B : 余额:19.9
单元格A : 余额:19.9
Cell:B : 已经接水时长:1
单元格A : 已经接水时长:1

单元格A : 余额:19.8
单元格A : 已经接水时长:2
Cell:B : 余额:19.8
Cell:B : 已经接水时长:2

单元格A : 余额:19.7
单元格A : 已经接水时长:3
Cell:B : 余额:19.7
Cell:B : 已经接水时长:3

单元格A : 结束取水

// 可以看到,当A结束时,就把tom已经消费的金额减去了

Cell:B : 余额:19.3
Cell:B : 已经接水时长:4

Cell:B : 余额:19.2
Cell:B : 已经接水时长:5

Cell:B : 余额:19.1
Cell:B : 已经接水时长:6

Cell:B : 余额:19.0
Cell:B : 已经接水时长:7

Cell:B : 余额:18.9
Cell:B : 已经接水时长:8

Cell:B : 余额:18.8
Cell:B : 已经接水时长:9

Cell:B : 余额:18.7
Cell:B : 已经接水时长:10

Cell:B : 余额:18.6
Cell:B : 已经接水时长:11

Cell:B : 余额:18.5
Cell:B : 已经接水时长:12

Cell:B : 余额:18.4
Cell:B : 已经接水时长:13

Cell:B : 余额:18.3
Cell:B : 已经接水时长:14

Cell:B : 余额:18.2
Cell:B : 已经接水时长:15

Cell:B : 结束取水

[BillDO{id='de727eb9-1e51-46ac-894d-0a00a4538931', time=2021-12-11T21:25:56.567896200, money=0.3}, 
BillDO{id='ac36b0ff-11e0-498c-944a-877ceebbb2df', time=2021-12-11T21:26:08.531859800, money=1.5}]
tom余额: 18.2
复制代码

测试代码如下:
【ps:因为junit好像不支持多线程,所以就一个一个方法执行吧】

package com.concurrent.water.machine;

import com.concurrent.water.machine.util.SimpleSleep;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

class MainTest {
    public static void main(String[] args) {
        MainTest mainTest = new MainTest();

        mainTest.singleUserUsed();
//        mainTest.differUserUsed();
//        mainTest.sameUserUsedInOneTime();
//        mainTest.differMachineSameUserUsedInOneTime();
    }

    @DisplayName("单个用户使用一台机器")
    void singleUserUsed() {
        ICell cellA = new Cell(new DefaultDisplayer("单元格A"));
        ICell cellB = new Cell(new DefaultDisplayer("Cell:B"));

        Machine machine = new DefaultMachine(cellA, cellB);

        User user = new User(BigDecimal.valueOf(20), new DefaultBillManager<>());

        // 用户使用机器打水多久
        long userUsedSeconds = 3;
        // 用户使用中
        machine.userUseCellA(user, TimeUnit.SECONDS, userUsedSeconds);

        new Thread(() -> {
            // 因为多线程的原因,模拟用户在程序执行完毕之后查看账单和余额的行为
            SimpleSleep.sleep(TimeUnit.SECONDS, userUsedSeconds + 2);
            System.out.println(user.getBills());
            Assertions.assertEquals(BigDecimal.valueOf(19.7), user.getBalance());
        }).start();
    }

    @DisplayName("不同用户使用一台机器")
    void differUserUsed() {
        ICell cellA = new Cell(new DefaultDisplayer("单元格A"));
        ICell cellB = new Cell(new DefaultDisplayer("Cell:B"));

        Machine machine = new DefaultMachine(cellA, cellB);

        User tom = new User(BigDecimal.valueOf(20), new DefaultBillManager<>());
        User jack = new User(BigDecimal.valueOf(18), new DefaultBillManager<>());

        // 用户使用机器打水多久
        long tomUsedSeconds = 3;
        long jackUsedSeconds = 5;

        machine.userUseCellA(tom, TimeUnit.SECONDS, tomUsedSeconds);
        machine.userUseCellB(jack, TimeUnit.SECONDS, jackUsedSeconds);

        long maxUsedTime = Math.max(tomUsedSeconds, jackUsedSeconds);

        new Thread(() -> {
            SimpleSleep.sleep(TimeUnit.SECONDS, maxUsedTime + 2);
            System.out.println(tom.getBills());
            System.out.println(jack.getBills());

            System.out.println("tom余额: "+ tom.getBalance());
            System.out.println("jack余额: " + jack.getBalance());

            Assertions.assertEquals(BigDecimal.valueOf(19.7), tom.getBalance());
            Assertions.assertEquals(BigDecimal.valueOf(17.5), jack.getBalance());
        }).start();
    }

    @DisplayName("相同用户同时使用一台机器的两个单元格")
    void sameUserUsedInOneTime() {
        ICell cellA = new Cell(new DefaultDisplayer("单元格A"));
        ICell cellB = new Cell(new DefaultDisplayer("Cell:B"));

        Machine machine = new DefaultMachine(cellA, cellB);

        User tom = new User(BigDecimal.valueOf(20), new DefaultBillManager<>());

        // 用户使用机器打水多久
        long firstUsedSeconds = 3;
        long secondUsedSeconds = 15;

        machine.userUseCellA(tom, TimeUnit.SECONDS, firstUsedSeconds);
        machine.userUseCellB(tom, TimeUnit.SECONDS, secondUsedSeconds);

        long maxSeconds = Math.max(firstUsedSeconds, secondUsedSeconds);

        new Thread(() -> {
            SimpleSleep.sleep(TimeUnit.SECONDS, maxSeconds + 2);
            System.out.println(tom.getBills());
            System.out.println("tom余额: "+ tom.getBalance());
            Assertions.assertEquals(tom.getBalance(), BigDecimal.valueOf(18.2));
        }).start();
    }

    @DisplayName("相同用户同时使用2台机器的两个单元格, 也就是同时使用4个单元格")
    void differMachineSameUserUsedInOneTime() {
        ICell cellA = new Cell(new DefaultDisplayer("机器1:单元格A"));
        ICell cellB = new Cell(new DefaultDisplayer("机器1:Cell:B"));

        Machine machine = new DefaultMachine(cellA, cellB);

        ICell cell2A = new Cell(new DefaultDisplayer("机器2:单元格A"));
        ICell cell2B = new Cell(new DefaultDisplayer("机器2:Cell:B"));

        Machine machine2 = new DefaultMachine(cell2A, cell2B);

        User tom = new User(BigDecimal.valueOf(20), new DefaultBillManager<>());

        // 用户使用机器打水多久
        long firstUsedSeconds = 3;
        long secondUsedSeconds = 15;
        long thirdUsedSeconds = 2;
        long forthUsedSeconds = 12;

        machine.userUseCellA(tom, TimeUnit.SECONDS, firstUsedSeconds);
        machine.userUseCellB(tom, TimeUnit.SECONDS, secondUsedSeconds);

        machine2.userUseCellA(tom, TimeUnit.SECONDS, thirdUsedSeconds);
        machine2.userUseCellB(tom, TimeUnit.SECONDS, forthUsedSeconds);

        long[] secondsArray = new long[]{firstUsedSeconds, secondUsedSeconds, thirdUsedSeconds, forthUsedSeconds};
        Arrays.sort(secondsArray);
        long maxSeconds = secondsArray[3];

        new Thread(() -> {
            SimpleSleep.sleep(TimeUnit.SECONDS, maxSeconds + 2);
            System.out.println(tom.getBills());
            Assertions.assertEquals(BigDecimal.valueOf(16.8), tom.getBalance());
        }).start();
    }
}
复制代码