问题一:当A,B同时攻击C时,需要对C进行减血逻辑。如果A,B是在不同线程执行这个逻辑的,那么会引发C的血量异常问题。
解决方案:
将减血逻辑放在一个单独的线程执行。具体操作为,首先,创建一个MainMsgProcessor的单例工具类
public class MainMsgProcessor {
static final MainMsgProcessor instance = new MainMsgProcessor();
private MainMsgProcessor(){
}
public static MainMsgProcessor getInstance(){
return instance;
}
public void process(){
//消息处理逻辑
}
}
我们用这个类统一处理消息,由他接管Netty的多线程handler处理消息。
怎么使用单线程处理呢?这里可以使用Java util concurrent包下面的
private final ExecutorService es = Executors.newSingleThreadExecutor();
创建一个单线程的线程池,将消息逻辑交由这个线程处理
因此process方法修改如下
public void process(){
es.submit(()->{
//处理消息逻辑
});
}
这样对业务逻辑的处理都会被交给这个单线程的线程池来处理。这样既避免了线程不安全的问题,又避免了业务逻辑与线程逻辑交杂的混乱。因为对于这个问题,我们可以在扣血的方法加上synchronized关键字,但是这样一是线程争夺锁导致效率降低,二是业务逻辑与线程安全逻辑混合,增加了编码的复杂度
线程池的内部是一个阻塞队列,因此这个问题的总体抽象如下图所示
可能有的人会有这样的疑问:将所有消息处理逻辑放在单线程,会不会导致并发量太低,速度慢?其实不然,因为游戏服务器速度的瓶颈都出现在IO和网络请求,或者是内存占用率太高,在业务逻辑的计算这一块单线程不见得会比多线程慢
问题二:在解决了上述问题之后,如果我们的登陆操作的消息处理也被放入同一个线程,那么每次用户的登陆请求都涉及到对数据库的访问,会导致其他请求被阻塞
解决方案:对IO实行多线程管理。IO操作被放入其他线程当作一个异步操作
我们首先定义一个静态的单例异步操作处理器,同样,我们也给他一个单线程的线程池
public final class AsyncOperationProcessor {
static final AsyncOperationProcessor instance = new AsyncOperationProcessor();
private AsyncOperationProcessor(){}
private final ExecutorService es = Executors.newSingleThreadExecutor();
public static AsyncOperationProcessor getInstance(){
return instance;
}
/**
* 执行异步操作
*
*
* @param r
*/
public void process(Runnable r){
if(r==null){
return;
}
es.submit(r);
}
}
这样,我们可以把登陆部分的代码逻辑封装入这个类中,让登陆逻辑异步执行
AsyncOperationProcessor.getInstance().process(()->{
//登陆逻辑
});
当我们执行完登陆逻辑的时候,我们还需要拿到登陆逻辑的返回结果,这也就是所谓的异步:执行完了之后会将结果交到你的手中。那么,我们如何拿到返回结果呢?在这个例子中,假如我们登陆操作返回了一个user对象,我们怎样拿到这个对象呢?这也是异步调用的通用问题
首先,第一种很容易想到的思路是,在外部定义一个变量,然后执行完之后将结果赋予这个外部的变量。
public User userLogin(String username,String password){
User user = null;
AsyncOperationProcessor.getInstance().process(()->{
//登陆逻辑
user = myUser
});
return user;
}
这个思路是行不通的,因为process是在另一个线程中执行的,也就是说,当前线程并不会等另一个线程全部执行完才往下运行,所以我们返回的user很有可能是登陆逻辑没有执行完全的user,也就是一个null
解决这个问题的正确做法之一是,我们不能指望从userLogin这个方法立刻拿到返回值,而是传入一个回调函数,当我们计算出结果的时候,将结果作为参数传给这个回调函数并调用它。
public void userLogin(String username,String password,
Function<User,void> callback){
User user = null;
AsyncOperationProcessor.getInstance().process(()->{
//登陆逻辑
if(callback!=null){
callback.apply(myUser)
}
});
}
那么我的userLogin调用的过程中,传入的参数应该是这样的
LoginService.getInstance().userLogin(username,password,(user)->{
//当异步过程执行完后,会将计算结果作为user 传入
//登陆完毕的后续操作代码
}
)
这样就算完美了吗?其实不然,因为我们的回调函数实际上是在我们的异步线程中被调用的,也就是说,业务逻辑也是在异步线程中执行的,而这又会引发数据的脏读写问题,而我们理想的情况应该是,异步线程将执行完毕的结果传给我们的消息处理线程,我们用那个线程来执行业务逻辑。
那么,针对这个问题的解决方案是什么呢?
首先,我们定义一个接口 :
public interface IAsyncOperation {
/**
* 执行异步操作
*/
void doAsync();
/**
* 执行异步操作完成之后的逻辑
* 可以不实现
*/
default void doFinish(){}
}
我们对我们上面的异步处理器类的process方法进行修改
public final class AsyncOperationProcessor {
static final AsyncOperationProcessor instance = new AsyncOperationProcessor();
private AsyncOperationProcessor(){}
private final ExecutorService es = Executors.newSingleThreadExecutor();
public static AsyncOperationProcessor getInstance(){
return instance;
}
/**
* 执行异步操作
*
*
* @param op
*/
public void process(IAsyncOperation op){
if(op==null){
return;
}
es.submit(()->{
//执行异步逻辑
op.doAsync();
//执行完成逻辑
op.doFinish();
});
}
}
同时,我们在MainMsgProcessor类中加入一个重载的process方法
public class MainMsgProcessor {
static final MainMsgProcessor instance = new MainMsgProcessor();
private final ExecutorService es = Executors.newSingleThreadExecutor();
private MainMsgProcessor(){
}
public static MainMsgProcessor getInstance(){
return instance;
}
public void process(){
es.submit(()->{
//处理消息逻辑
});
}
public void process(Runnable r){
if(r==null){
return;
}
es.submit(r);
}
}
随后,我们更改AsyncOperationProcessor的process方法,将doFinish提交给主线程去写
/**
* 执行异步操作
*
*
* @param op
*/
public void process(IAsyncOperation op){
if(op==null){
return;
}
es.submit(()->{
//执行异步逻辑
op.doAsync();
//执行完成逻辑
MainMsgProcessor.getInstance().process(()->{
op.doFinish();
});
});
}
这样一来,我们只需要实现这个接口,将登陆逻辑放入doAsync,而将完成登陆后回调放在doFinish中执行。
注:AsyncGetUserEntity为实现IAsyncOperation接口的类,其中的doAsync执行的是登陆逻辑
问题三:这样一来虽然将登陆放到别的线程中执行,那么阻塞的问题不是依然没有解决吗?因为IO线程只有一个
解决方案:在线程池中多开辟几个线程
问题四:如果用户连按两次登陆按钮,两个线程会同时开始登陆过程,有可能造成数据库的数据发生异常,一个用户被注册了两遍
解决方案:对用户名进行hash,使得每个用户的任务只能提交给某个固定的线程