上一篇文章的最后我們提到了攻擊消息的處理還是有些問題磅甩,其實(shí)應(yīng)該還是比較嚴(yán)重的問題按摘,我們來演示一下走敌。
在攻擊處理器:UserAttkCmdHandler中添加一條日志打印并重啟服務(wù)器
if(null == targetUser){
//如果沒打到人,也推送一下攻擊孙援,這樣客戶端可以顯示攻擊動作检碗,否則沒有,影響體驗(yàn)
broadcastAttrResult(attkUserId,-1);
return;
}
//增加日志打印
LOGGER.info("當(dāng)前線程 = {}",Thread.currentThread().getName());
final int dmgPoint = 10;
targetUser.currHp = targetUser.currHp - dmgPoint;
然后分別開啟三個客戶端userId分別為1金拒,2兽肤,3,測試地址: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)一個問題资铡,兩個攻擊消息的處理是在兩個線程內(nèi)進(jìn)行的,那么問題就很明顯了幢码,多線程操作會帶來問題笤休。
1.模擬錯誤
如果有同學(xué)不太明白這種操作帶來的問題,那么我為了模擬這種問題編寫了測試代碼
在test中新建TestUser症副,和原程序無關(guān)哈
/**
* 測試用戶
*/
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)建測試類MultiThreadTest
首先測試一下店雅,用兩個線程對同一個數(shù)值進(jìn)行修改,和我們目前項(xiàng)目中的思想類似贞铣,當(dāng)currHp和我們的預(yù)期不一致時闹啦,會拋出異常
/**
* 兩條線程修改同一數(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) {
// 打印錯誤日志
ex.printStackTrace();
}
if (newUser.currHp != 98) {
throw new RuntimeException("當(dāng)前血量錯誤, currHp = " + newUser.currHp);
} else {
System.out.println("當(dāng)前血量正確");
}
}
測試:
static public void main(String[] argvArray) {
for (int i = 0; i < 10000; i++) {
System.out.println("第 " + i + "次測試");
(new MultiThreadTest()).test1();
}
}
當(dāng)多執(zhí)行幾次后,會發(fā)現(xiàn)是有報錯出現(xiàn)的辕坝,這種情況如果在血量上可能還可以接受窍奋,但是如果在用戶充值或者消費(fèi)上出現(xiàn)問題呢?
2.問題解決方案
當(dāng)然我們第一想法就是使用synchronized關(guān)鍵字加鎖實(shí)現(xiàn)酱畅,我也寫了對應(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) {
// 打印錯誤日志
ex.printStackTrace();
}
if (newUser.currHp != 98) {
throw new RuntimeException("當(dāng)前血量錯誤, currHp = " + newUser.currHp);
} else {
System.out.println("當(dāng)前血量正確");
}
}
這樣我們就發(fā)現(xiàn)琳袄,減血的代碼是正確的,并沒有報錯圣贸,但是考慮另一種情況就是我們在攻擊時挚歧,角色1在攻擊角色2的同時,角色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) {
// 打印錯誤日志
ex.printStackTrace();
}
}
這是我截取的打印信息滑负,當(dāng)我們使用test03時在张,卡在了第0次測試就不在繼續(xù)了,那我們查看一下信息:
1.在控制臺輸入:jps
2.在控制臺輸入:jstack + 進(jìn)程編號
3.查看錯誤信息:
可以簡單的看出:線程在等待其他線程釋放資源矮慕,其實(shí)可以很明顯的看出是死鎖的問題
4.錯誤分析及變成思路:
看到這里我們發(fā)現(xiàn)了使用synchronized好像解決不了問題帮匾,首先他的鎖比較重,會影響速度痴鳄,其實(shí)這是一個并不很重要的問題瘟斜,因?yàn)榻鉀Q速度問題遠(yuǎn)遠(yuǎn)比解決線程問題容易的多,主要還是因?yàn)樗麜硭梨i的問題痪寻,這個問題就是很大的問題了(我們這里模擬了)
當(dāng)然有的同學(xué)會說我們可以使用CAS的一些類螺句,但是這里我們只做一個簡單的計數(shù),這種CAS的方式對我們來說又太重了橡类。
解決方案:我們可以參考一下redis的做法蛇尚,那就是我們可以把攻擊的處理放到一個線程執(zhí)行,使其串行化顾画,可以很簡單的解決這種多線程的問題取劫。那么有的同學(xué)會有疑惑,這樣會不會犧牲執(zhí)行速度研侣,那答案是一定的谱邪。但是我們可以用最簡單的辦法去解決這種線程的問題,其次是使用這種方法庶诡,我們所有的計算都是基于內(nèi)存處理的惦银,其實(shí)速度并不慢,速度方面也可以參考redis灌砖,redis執(zhí)行起來慢么璧函?總結(jié)為一句話就是,處理線程問題要遠(yuǎn)比處理速度問題困難
那么在下一篇文章基显,我們會落地對我們分析的方案做出落地實(shí)現(xiàn)。