产品中遇到的问题,我们尝试去想解决方案,不过在这篇文章中,我将与大家分享几种常用的技术,包括拆分锁、并行数据结构、保护数据而不是代码和减少锁的范围是允许我们在没有任何工具的情况下检测死锁的技术。锁不是问题的根源,锁之间的竞争通常是在多线程代码中遇到性能问题时,一般抱怨的是锁的问题。毕竟,众所周知,锁会减慢程序速度并且可扩展性低。因此,如果您开始使用这种“常识”来优化您的代码,那么以后很可能会出现严重的并发问题。因此,了解竞争锁和非竞争锁的区别非常重要。当一个线程试图进入另一个线程正在执行的同步块或方法时,就会发生锁争用。该线程将被迫进入等待状态,直到第一个线程执行完同步块并释放监视器。当一次只有一个线程尝试执行同步代码区域时,锁保持未竞争状态。事实上,JVM已经针对非竞争情况和大多数应用程序中的同步进行了优化。无竞争锁在执行期间不会产生任何额外开销。因此,你不应该因为性能问题而抱怨锁,你应该抱怨锁竞争。考虑到这一点,让我们看看可以做些什么来减少竞争的可能性或持续时间。保护数据,而不是代码解决线程安全问题的一种快速方法是锁定整个方法的可访问性。例如,下面的示例尝试以这种方式构建一个在线扑克游戏服务器:){ListtablePlayers=tables.get(table.getId());if(tablePlayers.size()<9){tablePlayers.add(player);}}}publicsynchronizedvoidleave(Playerplayer,Tabletable){/*bodyskippedforbrevity*/}publicsynchronizedvoidcreateTable(){/*bodyskippedforbrevity*/}publicsynchronizedvoiddestroyTable(Tabletable){/*bodyskippedforbrevity*/}}作者的用意是好的——新玩家入桌时,必须保证桌上玩家人数不超过桌上可以容纳的玩家总数增加9。但是这个方案实际上需要控制玩家什么时候进入牌桌——即使服务器的访问量很小,那些等待释放锁的线程也势必会频繁触发系统比赛项目。包括检查账户余额和表限制在内的锁定块可能会显着增加调用操作的开销,这无疑会增加竞争的可能性和持续时间。解决方案的第一步是确保我们保护的是数据,而不是从方法声明移至方法主体的同步声明。对于上面的简单示例,可能没有太大变化。但是我们必须考虑整个游戏服务的接口,而不仅仅是一个join()方法。classGameServer{publicMap>tables=newHashMap>();publicvoidjoin(Playerplayer,Tabletable){synchronized(tables){if(player.getAccountBalance()>table.getLimit()){ListtablePlayers=tables.get(table.getId());if(tablePlayers.size()<9){tablePlayers.add(player);}}}}publicvoidleave(Playerplayer,Tabletable){/*bodyskippedforbrevity*/}publicvoidcreateTable(){/*bodyskippedforbrevity*/}publicvoiddestroyTable(Tabletable){/*bodyskippedforbrevity*/}}它可能只是影响整个班级行为方式的一个小变化。每当玩家加入一张桌子时,先前的同步方法都会锁定整个GameServer实例,从而与试图同时离开桌子的玩家进行比赛。将锁从方法声明中移到方法体中延迟了锁的加载,从而减少了锁争用的可能性。#p#减少锁的范围现在,当我们确定我们需要保护数据而不是程序时,我们应该确保我们只在必要的地方加锁——例如,当上面的代码被重构时:publicclassGameServer{publicMap>tables=newHashMap>();publicvoidjoin(Playerplayer,Tabletable){if(player.getAccountBalance()>table.getLimit()){synchronized(tables){ListtablePlayers=tables.get(table.getId());if(tablePlayers.size()<9){tablePlayers.add(player);}}}}//othermethodsskippedforbrevity}这部分包含玩家账户余额检测可能会耗时的代码(可能会引起IO操作),移到lockbeyond控制范围。请注意,现在锁定仅用于防止玩家数量超过桌子容量,检查帐户余额不再是此保护的一部分。拆分锁从上面例子的第一行代码可以清楚的看出:整个数据结构都被同一个锁保护着。考虑到在一个数据结构中可能有几千张表,而且我们要保护任何一张表的人数不超过容量,在这种情况下发生争用事件的风险仍然很高。一个简单的方法是为每个表引入单独的锁,如下例所示:getLimit()){ListtablePlayers=tables.get(table.getId());synchronized(tablePlayers){if(tablePlayers.size()<9){tablePlayers.add(player);}}}}//othermethodsskippedforbrevity}现在,我们只同步单个表的可访问性,而不是所有表,这显着降低了锁争用的可能性。举一个具体的例子,现在我们的数据结构中有100个扑克牌表实例,现在发生比赛的机会比以前小100倍。#p#使用线程安全的数据结构另一个可以改进的地方是放弃传统的单线程数据结构,使用明确设计为线程安全的数据结构。例如,当使用ConcurrentHashMap存储您的表实例时,代码可能如下所示:/*Methodbodyskippedforbrevity*/}publicsynchronizedvoidcreateTable(){Tabletable=newTable();tables.put(table.getId(),table);}publicsynchronizedvoiddestroyTable(Tabletable){tables.remove(table.getId());}}在join()和leave()方法里面的同步块还是和前面的例子一样,因为我们要保证单表数据的完整性。ConcurrentHashMap在这里没有帮助。但是我们仍然会在increateTable()和destroyTable()方法中使用ConcurrentHashMap来创建和销毁新表,所有这些操作对于ConcurrentHashMap来说都是完全同步的,这允许我们并行地添加或减少表的数量。其他一些降低锁可见度的提示和技巧。在上面的示例中,锁定被声明为公开(对外界可见),这可能会让别有用心的人通过锁定您精心设计的显示器来破坏您的工作。检查java.util.concurrent.locksAPI,看看是否有其他的锁策略已经实现,并使用它们来改进上面的解决方案。使用原子操作。上面使用的简单递增计数器实际上不需要锁定。在上面的例子中,使用AtomicInteger而不是Integer作为计数器更合适。最后,不管你是在使用Plumber的自动死锁检测方案,还是手动从threaddump中获取方案信息,希望本文能帮助你解决锁竞争问题。
