[多线程之间通过队列倒数据]1.以转账和1读1写依然不安全为例子 2.想清楚数据是哪个线程维护的并始终让这个线程负责数据的修改(核心)

例子1(转账:想清楚这个数据由哪个线程维护,并负责到底)

Constant.java

package org.example.account.constant;

public class Constant {
    /**
     * 有多少个账户
     */
    public static int ACCOUNT_NUM = 100;

    /**
     * 每个人初始化的金币
     */
    public static int MONEY = 100;

    /**
     * 发起转账数量
     */
    public static int TRANSFER_NUM = 10000000;
}

Account.java

package org.example.account.model;

public class Account {
    /**
     * 玩家标识
     */
    private int id;
    /**
     * 玩家当前拥有的金钱数
     */
    private int money;

    public Account(int money, int id) {
        this.money = money;
        this.id = id;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public int getId() {
        return id;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", money=" + money +
                '}';
    }
}

AccountService.java

package org.example.account.service;

import org.example.account.model.Account;

import java.util.concurrent.CountDownLatch;

public class AccountService {
    public static void transferSafe(Account a, Account b, int num, CountDownLatch c) {
        // 模拟Netty收到a账户向b账户发起转账
        TaskExecuteService.getEsAtIndex(a.getId()).submit(() -> {
            if (a.getMoney() >= num) {
                a.setMoney(a.getMoney() - num);

                // 对b的修改确保在b的线程
                TaskExecuteService.getEsAtIndex(b.getId()).submit(() -> {
                    b.setMoney(b.getMoney() + num);

                    c.countDown();
                });
            } else {
                c.countDown();
            }
        });
    }

    public static void transferUnSafe(Account a, Account b, int num, CountDownLatch c) {
        // 模拟a账户发起转账
        TaskExecuteService.getEsAtIndex(a.getId()).submit(() -> {
            if (a.getMoney() >= num) {
                a.setMoney(a.getMoney() - num);

                // 修改b的数据也在a所在的线程,这让多个线程都会对b的数据进行修改,是线程不安全的
                b.setMoney(b.getMoney() + num);

                c.countDown();
            } else {
                c.countDown();
            }
        });
    }

}

TaskExecuteService.java

package org.example.account.service;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TaskExecuteService {
    private static ExecutorService[] esArray = new ExecutorService[8];

    public static void init() {
        for (int i = 0; i < esArray.length; i++) {
            esArray[i] = Executors.newSingleThreadExecutor();
        }
    }

    /**
     * Netty业务线程池
     *
     * @param index
     * @return
     */
    public static ExecutorService getEsAtIndex(int index) {
        return esArray[index % esArray.length];
    }
}

AccountTest.java

package org.example.account;

import org.example.account.constant.Constant;
import org.example.account.model.Account;
import org.example.account.service.AccountService;
import org.example.account.service.TaskExecuteService;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;

public class TestAccount {
    @Test
    public void test1() throws InterruptedException, IOException {

        // Netty业务线程初始化
        TaskExecuteService.init();

        // 初始化账户
        Account[] accountArray = new Account[Constant.ACCOUNT_NUM];
        for (int i = 0; i < accountArray.length; i++) {
            accountArray[i] = new Account(Constant.MONEY, i);
        }

        // 用于等待所有的任务执行结束
        CountDownLatch c = new CountDownLatch(Constant.TRANSFER_NUM);

        // 模拟相互转账业务
        for (int i = 0; i < Constant.TRANSFER_NUM; i++) {
            // 转账金额
            int num = ThreadLocalRandom.current().nextInt(1, 10);

            int index1 = ThreadLocalRandom.current().nextInt(0, accountArray.length);
            int index2 = ThreadLocalRandom.current().nextInt(0, accountArray.length);


            Account a1 = accountArray[index1];
            Account a2 = accountArray[index2];

//            AccountService.transferUnSafe(a1, a2, num, c); // 实际:5352 期望:10000

            AccountService.transferSafe(a2, a1, num, c); // 实际:10000 期望:10000

        }

        // 等待任务执行结束
        c.await();

        // 验证下数据正确性
        int totalMoney = 0;

        for (int i = 0; i < accountArray.length; i++) {
            totalMoney += accountArray[i].getMoney();
        }

        System.out.println("实际:" + totalMoney + " 期望:" + Constant.ACCOUNT_NUM * Constant.MONEY);

    }
}

总结1:想清楚这个数据是 哪个线程维护的,让对这个数据的操作始终由这个线程执行,避免多个线程篡改。

可见,对一个玩家的数据操作只发生在一个线程,那么,是可以保证线程安全的。

其实想想,这种队列术,其实和actor的思想是一样的,确保同一时刻,只有一个线程对玩家数据会发生修改。 只不过skynet中的actor对cpu的利用率更高,因为:这个线程,是任何一个空闲的线程。 而我们这个则进行了线程的绑定,有可能部分cpu在睡大觉。

但是整体来说,已经是足够了。以为在面对较多的用户时,基本上每个cpu都在执行任务,不会有太多的空闲。

总结2:再次思考线程安全的问题:想清楚这个数据是 哪个线程维护的,让对这个数据的操作始终由这个线程执行,避免多个线程篡改。

再想想什么时候会出现并发安全的问题? 那一定是:多个线程对同一个实例变量写时,才会出现并发安全。那么其它线程可以读,但是修改数据的权利只会让维护数据的这个线程具有修改的权利,那么不就可以消除线程安全的问题了。

其实这句话并不完全正确,这句话我认为是保证了数据一致性是没问题,但是可能出现: 线程1读这个数据A,接着线程2修改数据A,但是线程1显示的其实还是oldA,有可能显示时是有问题,但是数据其实是对的。其实吧,UI上的显示,本来就是:可能是不准确的,这其实没有什么,下次刷新下UI就可以了,毕竟逻辑数据是对的。

总结3:一定要想清楚数据是哪个线程维护的。

再次思考一个案例:玩家0借用玩家1,2,3,4,5英雄的问题。玩家1~5肯定是各自维护自己英雄是否已经被借的数据信息 。 玩家0则维护了自己所借用的5个英雄,及其每个英雄的剩余的时间戳。

那么是否被借的标记是玩家1~5所维护。那么修改是否被借的信息,那么现在就想清楚了每个数据的修改应该在哪个线程处理的问题。

思考4:c#中有await可以实现单线程异步

由于ET中是单进程单线程,因此出现了IO的话,为了不阻塞这个业务线程,必须要让别的线程阻塞,但是C#语法糖是甜的,可以单线程实现异步,其实就类似于全局while,过一段时间询问下:干完没,干完了,就接着执行,但是写起来,则是很丝滑。但是既然是:网络IO,实际的业务逻辑,可能是在别的机器进程上执行的,c#的单线程异步,等于是:没有显式的回调。

例子2.通过rpc的形式要数据实现线程间通讯,从而避免因为中间状态导致的数据错误。

对共享资源进行只读,也可能读到脏数据,因为可能一个线程操作到一半,另外一个线程读取,这时是脏数据。

Resource.java

package org.example.testThread;

public class Resource {
    private String name;
    private String sex;

    public Resource(String name, String sex) {
        this.name = name;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Resource{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                '}' + Thread.currentThread().getName();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

线程不安全的做法:

Input.java

package org.example.testThread.method1;

import org.example.testThread.Resource;

public class Input implements Runnable {
    private Resource r;

    public Input(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        int x = 0;
        while (true) {
            if (x == 0) {
                r.setName("mike");
                r.setSex("男");
            } else {
                r.setName("丽丽");
                r.setSex("女");
            }
            x = (x + 1) % 2;
        }
    }
}

Output.java

package org.example.testThread.method1;

import org.example.testThread.Resource;

public class Output implements Runnable {
    private Resource r;

    public Output(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(r);
        }
    }
}

Main1.java

package org.example.testThread.method1;

import org.example.testThread.Resource;

public class Main1 {
    public static void main(String[] args) {
        Resource r = new Resource("mike", "男");
        Input in = new Input(r);
        Output out = new Output(r);

        new Thread(in).start();
        new Thread(out).start();
    }
}

/*
Resource{name='丽丽', sex='女'}Thread-1
Resource{name='mike', sex='男'}Thread-1
Resource{name='丽丽', sex='男'}Thread-1  // error
Resource{name='mike', sex='女'}Thread-1 // error
 */

线程安全的做法(通过rpc的方式,一个线程往另外一个线程要数据,从而避免中间状态)

Main2.java

package org.example.testThread.method2;

import org.example.testThread.Resource;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main2 {
    public static void main(String[] args) {
        ExecutorService input = Executors.newSingleThreadExecutor();
        ExecutorService output = Executors.newSingleThreadExecutor();

        input.submit(() -> {
            Resource r = new Resource("mike", "男");
            int x = 0;
            while (true) {
                if (x == 0) {
                    r.setName("mike");
                    r.setSex("男");
                } else {
                    r.setName("丽丽");
                    r.setSex("女");
                }
                x = (x + 1) % 2;

                // 模拟一个output线程发出请求到input线程拿到数据后,再次回到input线程
                output.submit(() -> {
                    System.out.println(r);
                });

            }
        });

    }
}

/*
Resource{name='mike', sex='男'}pool-2-thread-1
Resource{name='丽丽', sex='女'}pool-2-thread-1
Resource{name='丽丽', sex='女'}pool-2-thread-1
Resource{name='丽丽', sex='女'}pool-2-thread-1
Resource{name='丽丽', sex='女'}pool-2-thread-1
Resource{name='丽丽', sex='女'}pool-2-thread-1
Resource{name='丽丽', sex='女'}pool-2-thread-1
Resource{name='丽丽', sex='女'}pool-2-thread-1
Resource{name='mike', sex='男'}pool-2-thread-1
Resource{name='mike', sex='男'}pool-2-thread-1
Resource{name='丽丽', sex='女'}pool-2-thread-1
Resource{name='mike', sex='男'}pool-2-thread-1
Resource{name='mike', sex='男'}pool-2-thread-1
Resource{name='mike', sex='男'}pool-2-thread-1
 */

总结:通过hash取模的方式,想清楚数据由哪个线程维护,让它始终操作这个线程,其它线程想要数据,则通过rpc的方式即可。

这时,有些人的做法可能就是:通过wait和notify 及其 甚至synchronized关键字去实现数据的同步,其实这种一旦出现了锁操作,那么就停不下来了,接踵而来就是死锁的问题,这是很容易出问题的,业务层往往可以完全无锁化。游戏服务器中正确的方式就是通过BlockingQueue实现线程之间数据的相互导入,即可。

再比如:游戏服务器中的组队,有可能A线程要数据时,是 3个人,实际上过了几秒后,已经是4个人了,人已经满了,但是客户端还没刷新UI,此时客户端再请求进入,肯定是不成功的,此时由于组队数据始终通过hash始终让一个线程去负责修改,其它线程通过rpc的方式要数据,从而避免了线程安全问题。

再比如在zfoo中,有了rpc的支持,同步的请求后,是可以实现:在web层面虽然是多线程要数据,每次请求都是在不同的线程收到的数据,但是业务逻辑却可以通过rpc的方式在游戏服务器层面进行处理,同时同步的返回,此时就完全再次实现了结合mongodb实现数据存储的实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值