通常提到的是,线程安全性问题等可能会听到某些方面的视图和结束线程安全性和并发工具的结论。为了促进开发人员执行多线程编程,现代编程语言提供各种并发工具。但是,如果我们不完全了解他们的使用情况,解决问题和最佳实践,则盲目使用可能会导致一些凹坑,较小的损失性能,而且大不能确保在多行条件下业务逻辑的正确性。
以前有商务同学和我,并且在生产中遇到了一个奇怪的问题,有时获得的用户信息是其他的。可变 - 线程隔离,并通过方法或inter -class共享。如果用户信息的获取更昂贵(例如,从数据库查询用户信息),则threadLocal中的缓存数据更合适,但是为什么它似乎具有用户信息中的一个混乱错误?
使用Spring Boot创建Web应用程序,并使用ThreadLocal存储整数的值,以表示线程中存储的用户信息。该值最初是null.in业务逻辑中,我从ThreadLocal获得一次,然后将外部参数设置为ThreadLocal,以从当前上下文中模拟用户信息的逻辑,然后再次获取值,然后最终输出获得的值两次获得的值。和线程名称。
有理由认为,您第一次在设置用户信息之前获得的第一次应始终为null,但是我们必须意识到该程序在tomcat中运行,并且执行程序的线程是Tomcat的工作线程,而Tomcat的工作线程基于A名称建议,线程池将重复使用几个固定线程。将线程重复使用后,首次从ThreadLocal获得的值是其他用户的请求留下的值。这次,ThreadLocal中的用户信息是其他用户的信息。
为了更快地重现此问题,我在配置文件中设置了tomcat参数,将最大线程池的数量设置为1,以便相同的线程始终处理请求:
然后,用户2来到请求接口。这次,错误出现了。用户ID的第一和第二次收购分别为1和2。如果是正常的,则该数字应为null和2。很明显,用户1的信息是第一次获得的,因为Tomcat的线程池重复了线程。两个请求是相同的线程:HTTP-NIO-8080-EXEC-1。
该示例告诉我们,编写业务代码时,我们必须首先了解哪些线程将运行:
我们可能会抱怨学习多线程是没有用的,因为代码未打开代码。但实际上,我们可能没有意识到,在像Tomcat这样的Web服务器下运行的业务代码最初是在多线程环境中运行的(否则,接口不能支持如此高的并发性),并且不能认为它没有被明确打开。Multi -Threading将没有线程安全问题。
由于线程的创建更加昂贵,因此Web服务器通常会使用线程池来处理请求,这意味着线程将被重复使用。这次,当使用ThreadLocal工具存储一些数据时,您需要特别注意清除该数据代码运行后的数据集数据。如果您在代码中使用自定义线程池,则将遇到此问题。
在理解了这个知识点之后,我们修改该代码的方案是,在代码的最后代码块中,它明确清除了threadlocal中的数据。在这种方式上,即使以前,新请求也不会获得错误的用户信息使用线程。修改的代码如下:
可以验证运行该程序,并且第一个查询用户信息首次无法通过用户请求请求的错误:
ThreadLocal使用独家资源的方法来解决线程的安全问题。如果我们真的需要在线程之间拥有资源,该怎么办?此时,我们可能需要使用线程安全容器。
JDK 1.5之后启动的confurrenthashmap是一个高性能的线程-Safe Hash仪表容器。“线程安全性”词特别容易误解,因为ConcurrenthashMap只能确保提供的原子读取和写作操作提供了线程-SAFE。在大量的业务代码中看到了这种误解,例如以下场景。有一个包含900个元素的地图,现在将其添加到100个元素中。此补充操作由10个线程执行。开发人员错误地认为使用consurrenthashmap不是线程安全问题,因此我在不思考的情况下编写了以下代码:首先通过“代码逻辑的大小”方法获取当前的元素数量每个线程并计算contrenthashmap。如何补充许多元素,并且该值在日志中输出,然后通过putall方法添加缺失元素。
您可以从日志中看到:
最初的900尺寸达到了期望,需要填写100个元素。
Worker1线程查询需要填充的元素36,这不是100的倍数。Worker13线程查询是否需要填充的元素数量为负,并且显然已经结束了。哈希图项目的总数为1536,显然没有达到填充1,000的期望。
对于这个场景,我们可以举一个图像的示例。Concurrenthashmap就像一个大篮子。现在,这个篮子里有900颗橙子。我们希望这个篮子超过1000橙,即另外100颗橙子。有10名工人来做这件事。每个人都到达后,每个人都会计算需要补充多少橙子,最后将橙子放入篮子里。
同意该篮子本身可以确保在安装物体时不会影响多个工人。从其他角度来看,篮子里可能有964颗橙子和36颗橙子。
回到concurrenthashmap,我们需要注意Concurrenthashmap提供的限制或能力:
使用contrenthashmap并不意味着多个操作的状态是一致的。没有其他线程正在操作它。如果您需要确保需要手动链接。
聚合方法(例如大小,ISEMPTY和包含)可以反映在并发条件下同时的中间状态。因此,在并发的情况下,这些方法的返回值只能用作参考,不能用于过程控制。显然,使用尺寸方法计算差异是过程控制。输入聚合方法不能确保原子度。在Putall过程中获取数据可能会获取一些数据。
可以看出,只有一个线程查询需要100个元素,而其他九个线程查询不需要补充元素,地图大小为1000。过程,最好使用普通的hashmap。
实际上,这并不完全。concurrenthashmap提供了一些简单的原子性复合逻辑方法,可以使用这些方法来发挥其功能。这扩展了代码中的另一个问题:当使用某些类库提供的高级工具时,开发人员仍可能仍然可能以旧的方式使用这些新课程。因为他们不使用自己的特征,所以他们无法发挥自己的力量。
让我们看一下使用地图来计算次数的场景。该逻辑在商业代码中非常普遍。
使用conturrenthashmap与统计数据,密钥范围为10。
每次使用多达10个并发,1000万个循环操作,并积累随机密钥。
如果键不存在,则第一个设置值为1。
我们学习以前的课程,直接通过锁锁定地图,然后做出判断,读取当前的累积值,加上1,并保存累积的添加值的逻辑。此代码没有问题,但不能给出完整发挥concurrenthashmap的力量。改进的代码如下:
使用contrenthashmap的原子方法进行计算以制作复合逻辑操作,以确定该值是否存在该值。如果没有存在,则将lambda表达式的结果作为一个值,即创建新的longadder对象,并最终返回值。
由于Computerifabsent方法返回的值是LongAdder,因此它是一个线程表蓄能器,因此它可以直接调用其增量方法以进行累积。
这样,当安全地确保线程并将前7行代码替换为1行时,可以达到最终性能。
此测试代码没有特殊功能。使用秒表测试代码的两个部分的性能。最后,遵循地图中元素元素的数量,以及所有值的总和是否符合代码的正确性以验证代码。测试结果如下:
可以看出,与使用锁一起操作conturrenthashmap的方法相比,优化的代码已提高了10次。是由Java的不安全实现所实现的。在虚拟机级别,它确保了写作数据的原子性,这比锁定效率要高得多:
高级并发工具(例如ConcurrentHashMap)确实提供了一些高级API。只有通过完全理解其特征,我们才能最大程度地发挥其功率,并且不能使用它,因为它足够高且酷。
除了普遍的并发类(例如ConsurrentHashmap)外,我们的工具包中还有一些原始面孔,用于特殊场景。从总体上讲,对于通用场景的一般解决方案,在所有场景中都可以表现出色,属于“ 10,000金油”;而且,特殊场景的特殊实现将比一般解决方案具有更高的性能,但是它必须在IT中使用,否则可能会导致性能问题甚至错误。
在研究生产性能问题时,我们发现了一个简单的非数据库操作业务逻辑,该逻辑消耗了超出预期的时间,并且在修改数据时操作本地缓存比背面-WRITE数据库慢得多。查看数据。代码发现,开发学生使用CopyOnwritearRayList来缓存大量数据,并且数据更改更加频繁。
CoperOnwrite是一项时尚技术,无论是Linux还是Redis.in Java,尽管CopyOnWritearRayList是一个线程 - 安全阵列列表,因为它已实现,每次修改数据时都会复制数据的副本,因此有明显适用的适用场景,即,阅读更多写作或没有锁定读数。
如果我们想使用copyOnwritearRaylrist,那一定是因为场景的需求,而不是因为它足够酷。
让我们编写一个测试代码,以比较ArrayList与CopyOnwritearRaylist和普通锁定方法的读写性能。在此代码中,我们编写了一种用于并发读取和并发写作的测试方法,这需要时间来测试写作或阅读操作的时间。
可以看出,可以看出运行程序是大量的书面方案(100,000个添加操作),CopyOnWriteArray几乎比同步ArrayList慢了100倍:
在大量的阅读场景(100万GET操作)中,CopyOnWritearray的速度是同步Arraylist的五倍以上:
您可能会问,为什么在大量的写作方案中,CopyOnwritearRaylast列表如此慢?答案是在源代码中。以添加方法为例,每次添加,arrays arrays arrays.copyof将创建一个新数组。当频繁添加会消耗很多时,发行内存的应用程序:
原始:https://juejin.cn/post/7099729284574969863