✅能不能谈谈你对线程安全的理解?

典型回答

线程安全是指某个函数在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成

简单来说,就是多个线程同时访问共享变量的时候,得到的结果和我们预期的一样,就是线程安全。所以有四个关键词:并发、多线程、共享变量、正确完成。这里所谓的正确完成,其实就是要满足所谓的原子性、有序性和可见性。

知识扩展

并发与并行

✅什么是并发,什么是并行?

进程和线程

理解了并发和并行之间的关系和区别后,我们再回到前面介绍的多任务分时操作系统,看看CPU是如何进行进程调度的。

为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户的各个任务使用。

在多任务处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。在操作系统中,CPU切换到另一个进程需要保存当前进程的状态并恢复另一个进程的状态:当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作。

在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换帧”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

而在多个进程之间切换的时候,需要进行上下文切换。但是上下文切换势必会耗费一些资源。于是人们考虑,能不能在一个进程中增加一些“子任务”,这样减少上下文切换的成本。比如我们使用Word的时候,它可以同时进行打字、拼写检查、字数统计等,这些子任务之间共用同一个进程资源,但是他们之间的切换不需要进行上下文切换。

在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

随着时间的慢慢发展,人们进一步的切分了进程和线程之间的职责。把进程当做资源分配的基本单元,把线程当做执行的基本单元,同一个进程的多个线程之间共享资源

拿我们比较熟悉的Java语言来说,Java程序是运行在JVM上面的,每一个JVM其实就是一个进程。所有的资源分配都是基于JVM进程来的。而在这个JVM进程中,又可以创建出很多线程,多个线程之间共享JVM资源,并且多个线程可以并发执行。

但是,需要注意的是,Java中,在JDK21 出来虚拟线程之前,线程在操作系统层面也是基于轻量级进程实现的, 本质上还是存在操作系统级别的上下文切换的。JDK 21的虚拟线程是一种用户态线程,其切换不需要操作系统的参与,因此可以避免操作系统级别上下文切换,但是仍然需要在JVM层面做一些保存和恢复线程的状态,但是也成本低得多)

线程的特点

在多线程操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。

轻型实体

线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。 线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息: (1)线程状态。 (2)当线程不运行时,被保存的现场资源。 (3)一组执行堆栈。 (4)存放每个线程的局部变量主存区。 (5)访问同一个进程中的主存和其它资源。 用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

独立调度和分派的基本单位。

在多线程操作系统中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

可并发执行

在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

共享进程资源

在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

共享变量

所谓共享变量,指的是多个线程都可以操作的变量。

前面我们提到过,进程是分配资源的基本单位,线程是执行的基本单位。所以,多个线程之间是可以共享一部分进程中的数据的。在JVM中,Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可以操作保存在堆或者方法区中的同一个数据。那么,换句话说,保存在堆和方法区中的变量就是Java中的共享变量。

那么,Java中哪些变量是存放在堆中,哪些变量是存放在方法区中,又有哪些变量是存放在栈中的呢?

类变量、成员变量和局部变量

Java中共有三种变量,分别是类变量、实例变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。

1701952881526-6797919e-c18a-4bb0-867e-03431e501d85.png

这里有关于成员变量、局部变量,以及成员变量中类变量和实例变量的定义。

https://web.archive.org/web/20220412010257/http://www.dickbaldwin.com/java/Java020.htm

/**
* @author Hollis
*/
public class Variables {

    /**
    * 类变量
    */
    private static int a;

    /**
    * 成员变量
    */
    private int b;

    /**
    * 局部变量
    * @param c
    */
    public void test(int c){
        int d;
    }
}

上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。

所以,变量a和b是共享变量,变量c和d是非共享变量。所以如果遇到多线程场景,对于变量a和b的操作是需要考虑线程安全的,而对于变量c和d的操作是不需要考虑线程安全的。

但是也需要注意,即使是a和b这种变量,也不代表一定会有线程安全问题,如果没有写操作,或者对象没有被复用,那么也不会存在线程安全问题。

原文: https://www.yuque.com/hollis666/xkm7k3/bnddbd