什么是线程的安全性问题?说一下竞态、原子性、可见性、有序性

483 阅读6分钟

Hi 大家好,我是 DHL,大厂程序员,公众号:ByteCode ,在美团、快手、小米工作过。搞过逆向,做过性能优化,研究过系统,擅长鸿蒙、Android、Kotlin、性能优化、职场分享。

微信小程序「猿面试」每日分享一道大厂面试题,涉及 JavaAndroid鸿蒙和ArkTS设计模式算法和数据结构 等内容。


什么是线程的安全性问题?说一下竞态、原子性、可见性、有序性

在 Java 中,线程安全性问题主要指的是在多线程环境下,多个线程同时访问和修改共享资源时可能出现的不一致或错误状态。这些问题通常由以下几个原因引起:

竞态

当多个线程对同一共享资源进行非原子性的操作时,可能会导致结果不确定的情况。例如,两个线程同时读取一个变量的值并进行修改,可能会导致最终结果与预期不符。

竞态往往伴随着脏数据和丢失更新的问题。

  • 脏数据就是线程读到一个过时的数据

  • 丢失更新就是一个线程对数据做的更新,没有体现在后续其他线程对该数据的读取上。

对于共享变量,竞态可以看成访问 (读/写) 同一组共享变量的多个线程锁执行的操作相互交错,比如一个线程读取共享变量,并以该共享变量为基础进行计算的期间,另一个线程更新了该共享变量的值,导致脏数据或丢失更新。

对于局部变量,由于不同的线程各自访问的是自己的局部变量,所以局部变量的使用不会导致竞态。

原子性

原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

八种基本类型都具有原子性的。Java 内存模型只保证了基本读取和赋值是原子性操作(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)

如果要实现更大范围操作的原子性,可以通过 synchronizedLock 来实现的。由于 synchronizedLock 能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

可见性

可见性:一个线程对共享变量的更新,对于其他读取该变量的线程是否可见。

可见性问题与计算机的存储系统有关,程序中的变量可能会被分配到寄存器而不是主内存中,每个处理器都有自己的寄存器,一个处理器无法读取另一个处理器的寄存器上的内容。

Java 提供了 volatilesynchronized 关键字来保证可见性。

当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

通过 synchronizedLock 也能够保证可见性,synchronizedLock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

在 Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行的结果,却会影响到多线程并发执行的正确性。

在本线程内,操作都是有序的;在线程外,操作都是无序的。在 Java 内存模型中,允许编译器和处理对执行进行重排序,但是重排序过程不会影响到单线程程序的执行,却影响到多线程并发执行的正确性。

在 Java 里面,可以通过 volatile 关键字来保证一定的"有序性",volatile 关键字会禁止指令重排。

synchronized 关键字保证同一时刻只允许一条线程操作,通过 synchronizedLock 来保证有序性,很显然,synchronizedLock 保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

更多大厂面试题,欢迎前往微信搜索小程序 「猿面试」 查看。微信小程序 (猿面试) 包含了 Java、Android、鸿蒙和ArkTS设计模式算法和数据结构 相关内容,

Hi 大家好,我是 DHL,大厂程序员,公众号:ByteCode ,在美团、快手、小米工作过。搞过逆向,做过性能优化,研究过系统,擅长鸿蒙、Android、Kotlin、性能优化、职场分享。

更多面试题