编写线程安全类时,主要问题是将数据分为多个独立的部分,并为这些部分选择合适的大小。如果部分太小,则我们的类不是线程安全的。如果部件太大,则该类不可扩展。
让我们看一个进一步说明这种情况的示例:
一个例子
假设我们要跟踪一个城市中有多少人。我们要支持两种方法,一种是获取当前居住在城市中的人数,另一种是将一个人从一个城市转移到另一个城市。因此,我们有以下界面:
由于我们要从多个线程并行使用此接口,因此必须选择实现此接口的选项。使用该类 java.util.concurrent. ConcurrentHashMap
或使用该类 java.util.HashMap
和一个锁。这是使用类的实现 java.util.concurrent.ConcurrentHashMap
:
方法移动使用线程安全方法计算来减少源城市中的计数。然后,使用计算来增加目标城市中的计数。count方法使用线程安全方法 get
。
这是使用该类的实现java.util.HashMap
:
该方法move
还使用该方法compute
来增加和减少源城市和目标城市中的计数。仅在这一次,因为该compute
方法不是线程安全的,所以两个方法都被同步块包围。该 count
方法get
再次使用被同步块包围的方法。
两种解决方案都是线程安全的。
但是在使用的解决方案中ConcurrentHashMap
,可以从不同线程并行更新多个城市。在使用a的解决方案中HashMap
,由于锁是在complete周围HashMap
,因此HashMap
在给定时间只有一个线程可以更新。因此,使用的解决方案ConcurrentHashMap
应该更具可扩展性。让我们来看看。
太大意味着无法扩展
为了比较这两种实现的可伸缩性,我使用以下基准:
基准测试使用jmh(一种用于微基准测试的OpenJDK框架)。在基准测试中,我将人们从一个城市转移到另一个城市。每个工作线程都会更新不同的城市。源城市的名称只是线程ID,目标城市的名称是线程ID加两个。我在Intel i5 4核心CPU上运行了基准测试,结果如下:
如我们所见,使用该解决方案的ConcurrentHashMap
规模更好。从两个线程开始,它的性能要优于使用单个锁的解决方案。
太小意味着线程不安全
现在,我需要一种其他方法来获得整个城市的总数。这是使用类的实现方法ConcurrentHashMap
:
要查看此解决方案是否是线程安全的,我使用以下测试:
我需要两个线程来测试该方法completeCount
是否是线程安全的。一口气,我将一个人从斯普林菲尔德搬到了南方公园。在另一个线程中,我得到 completeCount
并检查结果是否等于预期结果。
为了测试所有线程交织,我们使用AllInterleavings
来自vmlens的类(第7行)遍历所有线程交织,将完整的测试放在while循环中 。运行测试,我看到以下错误:
vmlens报告显示出了什么问题:
如我们所见,问题在于完成计数的计算是在另一个线程仍将一个人从斯普林菲尔德移到南方公园的同时进行的。Springfield的减量已经执行,但South Park的增量没有执行。
通过允许并行更新不同的城市,将completeCount
和 之间的组合move
会导致错误的结果。如果我们有适用于所有城市的方法,则需要在此方法期间锁定所有城市。因此,为了支持这种方法,我们需要使用带有单个锁的第二个解决方案。对于此解决方案,我们可以实现一个线程安全的countComplete
方法,如下所示:
结论
这个简单的例子肯定不会反映出数据结构的复杂性。但是,在示例中正确的事实在现实世界中也是正确的。除了以一个线程一个线程地更新另一个从属字段之外,无法以线程安全的方式更新多个从属字段。
除了以一个线程一个线程地更新另一个从属字段之外,无法以线程安全的方式更新多个从属字段。
因此,实现可伸缩性和线程安全性的唯一方法是在数据中找到独立的部分。然后,从多个线程并行更新它们。
有什么问题可以加下qq:2062583349。也可添加vx:admindesire,有java、python、web等习资料和视频课程干货”。欢迎交流!