这样那段包含对玩家账号余额检测(可能引发IO操作)的可能引起费时操作的代码,被移到了锁控制的范围之外。注意,现在锁仅仅被用来防止玩家人数超过桌子可容纳的人数,对账户余额的检查不再是该保护措施的一部分了。
分离锁
你可以从上面例子最后一行代码清楚的看到:整个数据结构是由相同的锁保护着。考虑到在这一种数据结构中可能会有数以千计的牌桌,而我们必须保护任何一张牌桌的人数不超过容量,在这样的情况下仍然会有很高的风险出现竞争事件。
关于这个有一个简单的办法,就是对每一张牌桌引入分离锁,如下面这个例子所示:
public class GameServer {
public Map> tables = new HashMap>();
public void join(Player player, Table table) {
if (player.getAccountBalance() > table.getLimit()) {
List tablePlayers = tables.get(table.getId());
synchronized (tablePlayers) {
if (tablePlayers.size() < 9) {
tablePlayers.add(player);
}
}
}
}
//other methods skipped for brevity
}
现在,我们只对单一牌桌的可访问性进行同步而不是所有的牌桌,这样就显著降低了出现锁竞争的可能性。举一个具体的例子,现在在我们的数据结构中有100个牌桌的实例,那么现在发生竞争的可能性就会比之前小100倍。
使用线程安全的数据结构
另一个可以改善的地方就是抛弃传统的单线程数据结构,改用被明确设计为线程安全的数据结构。例如,当采用ConcurrentHashMap来储存你的牌桌实例时,代码可能像下面这样:
public class GameServer {
public Map> tables = new ConcurrentHashMap>();
public synchronized void join(Player player, Table table) {/*Method body skipped for brevity*/}
public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/}
public synchronized void createTable() {
Table table = new Table();
tables.put(table.getId(), table);
}
public synchronized void destroyTable(Table table) {
tables.remove(table.getId());
}
}
在join()和leave()方法内部的同步块仍然和先前的例子一样,因为我们要保证单个牌桌数据的完整性。ConcurrentHashMap 在这点上并没有任何帮助。但我们仍然会在increateTable()和destoryTable()方法中使用ConcurrentHashMap创建和销毁新的牌桌,所有这些操作对于ConcurrentHashMap来说是完全同步的,其允许我们以并行的方式添加或减少牌桌的数量。
其他一些建议和技巧
降低锁的可见度。在上面的例子中,锁被声明为public(对外可见),这可能会使得一些别有用心的人通过在你精心设计的监视器上加锁来破坏你的工作。
通过查看Java.util.concurrent.locks 的API来看一下 有没有其它已经实现的锁策略,使用其改进上面的解决方案。
使用原子操作。在上面正在使用的简单递增计数器实际上并不要求加锁。上面的例子中更适合使用 AtomicInteger代替Integer作为计数器。
最后一点,无论你是否正在使用Plumber的自动死锁检测解决方案,还是手动从线程转储获得解决办法的信息,都希望这篇文章可以为你解决锁竞争的问题带来帮助。