上一篇文章的最后我們提到了攻擊消息的處理還是有些問(wèn)題,其實(shí)應(yīng)該還是比較嚴(yán)重的問(wèn)題,我們來(lái)演示一下。
在攻擊處理器:UserAttkCmdHandler中添加一條日志打印并重啟服務(wù)器
if(null == targetUser){
//如果沒(méi)打到人,也推送一下攻擊,這樣客戶端可以顯示攻擊動(dòng)作,否則沒(méi)有,影響體驗(yàn)
broadcastAttrResult(attkUserId,-1);
return;
}
//增加日志打印
LOGGER.info("當(dāng)前線程 = {}",Thread.currentThread().getName());
final int dmgPoint = 10;
targetUser.currHp = targetUser.currHp - dmgPoint;
然后分別開(kāi)啟三個(gè)客戶端userId分別為1,2,3,測(cè)試地址:http://cdn0001.afrxvk.cn/hero_story/demo/step020/index.html?serverAddr=127.0.0.1:12345&userId=1,再分別使用角色1和角色2攻擊角色3,觀察日志打印
[INFO] UserAttkCmdHandler.handle --> 當(dāng)前線程 = nioEventLoopGroup-3-5
[21:18:05,794] [INFO] GameMsgHandler.channelRead0 --> 收到客戶端消息, msgClzz=com.tk.tinygame.herostory.msg.GameMsgProtocol$UserAttkCmd,msgBody = targetUserId: 3
com.tk.tinygame.herostory.cmdhandler.UserAttkCmdHandler@2232bd6b
[21:18:05,795] [INFO] UserAttkCmdHandler.handle --> 當(dāng)前線程 = nioEventLoopGroup-3-6
可以發(fā)現(xiàn)一個(gè)問(wèn)題,兩個(gè)攻擊消息的處理是在兩個(gè)線程內(nèi)進(jìn)行的,那么問(wèn)題就很明顯了,多線程操作會(huì)帶來(lái)問(wèn)題。
1.模擬錯(cuò)誤
如果有同學(xué)不太明白這種操作帶來(lái)的問(wèn)題,那么我為了模擬這種問(wèn)題編寫了測(cè)試代碼
在test中新建TestUser,和原程序無(wú)關(guān)哈
/**
* 測(cè)試用戶
*/
public class TestUser {
/**
* 當(dāng)前血量
*/
public int currHp;
/**
* 減血
*
* @param val
*/
synchronized public void subtractHp(int val) {
if (val <= 0) {
return;
}
this.currHp = this.currHp - val;
}
/**
* 攻擊
*
* @param targetUser
*/
public void attkUser(TestUser targetUser) {
if (null == targetUser) {
return;
}
synchronized (this) {
final int dmgPoint = 10;
targetUser.subtractHp(dmgPoint);
}
}
}
創(chuàng)建測(cè)試類MultiThreadTest
首先測(cè)試一下,用兩個(gè)線程對(duì)同一個(gè)數(shù)值進(jìn)行修改,和我們目前項(xiàng)目中的思想類似,當(dāng)currHp和我們的預(yù)期不一致時(shí),會(huì)拋出異常
/**
* 兩條線程修改同一數(shù)值
*/
private void test1() {
TestUser newUser = new TestUser();
newUser.currHp = 100;
Thread t0 = new Thread(() -> { newUser.currHp = newUser.currHp - 1; });
Thread t1 = new Thread(() -> { newUser.currHp = newUser.currHp - 1; });
t0.start();
t1.start();
try {
t0.join();
t1.join();
} catch (Exception ex) {
// 打印錯(cuò)誤日志
ex.printStackTrace();
}
if (newUser.currHp != 98) {
throw new RuntimeException("當(dāng)前血量錯(cuò)誤, currHp = " + newUser.currHp);
} else {
System.out.println("當(dāng)前血量正確");
}
}
測(cè)試:
static public void main(String[] argvArray) {
for (int i = 0; i < 10000; i++) {
System.out.println("第 " + i + "次測(cè)試");
(new MultiThreadTest()).test1();
}
}
當(dāng)多執(zhí)行幾次后,會(huì)發(fā)現(xiàn)是有報(bào)錯(cuò)出現(xiàn)的,這種情況如果在血量上可能還可以接受,但是如果在用戶充值或者消費(fèi)上出現(xiàn)問(wèn)題呢?

2.問(wèn)題解決方案
當(dāng)然我們第一想法就是使用synchronized關(guān)鍵字加鎖實(shí)現(xiàn),我也寫了對(duì)應(yīng)的代碼
/**
* 利用 synchronized 同步數(shù)據(jù)
*/
private void test2() {
TestUser newUser = new TestUser();
newUser.currHp = 100;
Thread t0 = new Thread(() -> { newUser.subtractHp(1); });
Thread t1 = new Thread(() -> { newUser.subtractHp(1); });
t0.start();
t1.start();
try {
t0.join();
t1.join();
} catch (Exception ex) {
// 打印錯(cuò)誤日志
ex.printStackTrace();
}
if (newUser.currHp != 98) {
throw new RuntimeException("當(dāng)前血量錯(cuò)誤, currHp = " + newUser.currHp);
} else {
System.out.println("當(dāng)前血量正確");
}
}
這樣我們就發(fā)現(xiàn),減血的代碼是正確的,并沒(méi)有報(bào)錯(cuò),但是考慮另一種情況就是我們?cè)诠魰r(shí),角色1在攻擊角色2的同時(shí),角色2也可以攻擊角色1,那就是另一種代碼的實(shí)現(xiàn)了
/**
* 死鎖
*/
private void test3() {
TestUser user1 = new TestUser();
user1.currHp = 100;
TestUser user2 = new TestUser();
user2.currHp = 100;
Thread t0 = new Thread(() -> { user1.attkUser(user2); });
Thread t1 = new Thread(() -> { user2.attkUser(user1); });
t0.start();
t1.start();
try {
t0.join();
t1.join();
} catch (Exception ex) {
// 打印錯(cuò)誤日志
ex.printStackTrace();
}
}

這是我截取的打印信息,當(dāng)我們使用test03時(shí),卡在了第0次測(cè)試就不在繼續(xù)了,那我們查看一下信息:
1.在控制臺(tái)輸入:jps

2.在控制臺(tái)輸入:jstack + 進(jìn)程編號(hào)

3.查看錯(cuò)誤信息:
可以簡(jiǎn)單的看出:線程在等待其他線程釋放資源,其實(shí)可以很明顯的看出是死鎖的問(wèn)題

4.錯(cuò)誤分析及變成思路:
看到這里我們發(fā)現(xiàn)了使用synchronized好像解決不了問(wèn)題,首先他的鎖比較重,會(huì)影響速度,其實(shí)這是一個(gè)并不很重要的問(wèn)題,因?yàn)榻鉀Q速度問(wèn)題遠(yuǎn)遠(yuǎn)比解決線程問(wèn)題容易的多,主要還是因?yàn)樗麜?huì)帶來(lái)死鎖的問(wèn)題,這個(gè)問(wèn)題就是很大的問(wèn)題了(我們這里模擬了)
當(dāng)然有的同學(xué)會(huì)說(shuō)我們可以使用CAS的一些類,但是這里我們只做一個(gè)簡(jiǎn)單的計(jì)數(shù),這種CAS的方式對(duì)我們來(lái)說(shuō)又太重了。
解決方案:我們可以參考一下redis的做法,那就是我們可以把攻擊的處理放到一個(gè)線程執(zhí)行,使其串行化,可以很簡(jiǎn)單的解決這種多線程的問(wèn)題。那么有的同學(xué)會(huì)有疑惑,這樣會(huì)不會(huì)犧牲執(zhí)行速度,那答案是一定的。但是我們可以用最簡(jiǎn)單的辦法去解決這種線程的問(wèn)題,其次是使用這種方法,我們所有的計(jì)算都是基于內(nèi)存處理的,其實(shí)速度并不慢,速度方面也可以參考redis,redis執(zhí)行起來(lái)慢么?總結(jié)為一句話就是,處理線程問(wèn)題要遠(yuǎn)比處理速度問(wèn)題困難
那么在下一篇文章,我們會(huì)落地對(duì)我們分析的方案做出落地實(shí)現(xiàn)。