java 线程安全问题

331 阅读4分钟

「这是我参与 2022 首次更文挑战的第 10 天,活动详情查看:2022 首次更文挑战

人之所助者,信也。

前言

线程安全问题是我们在工作和面试过程中经常遇到的问题,了解多线安全的问题就能在工作中和面试中显得游刃有余,即表明自己的专业素养,也为自己的知识架构添砖加瓦。

线程安全问题是什么

线程安全问题是指多个线程同时对于某个共享资源的访问,所导致的的原子性、有序性、和可见性的问题。而这些问题会导致程序的运行结果存在不可预测性,会出现超出预期的结果。线程安全问题的存在本身就是计算机 cpu、多级缓存架构、主内存在整个运行过程中如何保证多线程访问期间数据的一致性问题。

下图展示的就是两个线程由于线程切换,导致操作同一个变量时,导致最终的计算结果出现错误。

其主要原因就是多个线程是在不同的 CPU 上执行的,由于线程间没有进行通信,会导致读取和写入操作时数据发生错误,一般情况下计算机由缓存一致性协议来保证这一操作的原子性和可见性,但是对于开发者来说,这是远远不够的,因为这个协议包括 java 中常用的 final 关键字,也只是保证了单个操作的原子性,不能保证复合操作的有序性。单个操作简单的说就是赋值操作,复合操作可以理解为 i = i + 1之类的操作。

线程安全的解决方法

一般情况下解决线程安全的方法是增加同步锁,常见的像是使用 Synchronized Lock 等,由于导致线程安全的根本原因是多线程访问共享资源造成的,所以每个线程在访问共享资源前进行加锁操作就能够解决这个问题,同步锁的特征就是在任意时刻只允许一个线程访问共享资源,直到锁被释放为止。这种方式可以解决线程安全的问题,但是额外带来了加锁和释放锁的性能开销,降低了系统的并发度,同时也会导致线程从用户态到内核态的切换,进行上下文的切换。这里说的是单机的并发安全问题,在分布式系统中,相应的可以采用分布式锁来解决多应用多线程间访问共享资源带来的安全性问题。

如何在无锁和线程安全之间取得一个平衡,这就引出了一个无锁并发的概念:

1 第一个是使用自旋锁(本质就是 while 循环+CAS),是指线程在没有抢占锁的情况下通过自旋指定次数去获取锁,每一个循环根据需要还可以加上休眠时间,是为了解决 自旋带来的 cpu 开销过大的问题。

2 第二个是乐观锁。是指每个数据加上版本号,一旦数据发生变化则修改这个版本号,版本号一般是指自增序列或者是时间戳。在 java 中有一个 CAS 的机制来实现乐观锁的功能。

3 在程序开发过程中尽量减少共享对象的使用,从业务上进行隔离避免并发的,比如可以在程序中使用 ThreadLocal 来解决,线程访问资源隔离,来解决并发的问题。比如在日期转换中我们经常用到的 SimpleDataFormat ,如果我们使用其作为工具类使用,就可能导致并发的问题。

总结

java 开发过程中的线程安全问题是常见的,需要在编码时考虑多线程的问题,确保在多线程的环境下保证数据的安全性,业务的稳定性,这样健壮的代码才能体现程序员的编码功底,多线程情况下代码会比较复杂,但是不是那么难,需要多一分的耐心和认真。