什么是线程安全
线程安全指某个函数、函数库在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
并发
提到线程安全,必须要提及的一个词就是并发,如果没有并发,那么也就不存在线程安全问题了。
并发,在操作系统中,是指在一个时间段内同时有几个程序处于启动运行和运行完毕之间,且这几个程序在同一个处理机上运行。
那么,操作系统是如何实现这种并发的呢?
操作系统是把CPU的时间划分成长短基本相同的时间区间,即“时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个作业使用。
如果某个作业在时间片结束之前,整个任务还没有完成,那么该作业就被暂停下来,放弃CPU,等待下一轮循环再继续做。此时CPU又分配给另一个作业去使用。
所以,在单CPU的计算机中,我们看起来“同时干多件事”,其实是通过CPU时间片技术并发完成的。
提到并发,还有另外一个词容易和他混淆,那就是并行。
并行,当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行。
多线程
对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动一个浏览器进程,打开两个记事本就启动了两个记事本进程。
而在多个进程之间切换的时候,需要进行上下文切换。但是上下文切换势必会耗费一些资源。于是人们考虑,能不能在一个进程中增加一些“子任务”,这样减少上下文切换的成本。比如我们使用Word的时候,它可以同时进行打字、拼写检查、字数统计等,这些子任务之间共用同一个进程资源,但是他们之间的切换不需要进行上下文切换。
上下文切换就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作。
在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
随着时间的慢慢发展,人们进一步的切分了进程和线程之间的职责。把进程当做资源分配的基本单元,把线程当做执行的基本单元,同一个进程的多个线程之间共享资源。
拿Java语言来说,Java程序是运行在JVM上面的,每一个JVM其实就是一个进程。所有的资源分配都是基于JVM进程来的。而在这个JVM进程中,又可以创建出很多线程,多个线程之间共享JVM资源,并且多个线程可以并发执行。
共享变量
共享变量,指的是多个线程都可以操作的变量。
前面提到过,进程是分配资源的基本单位,线程是执行的基本单位。所以,多个线程之间是可以共享一部分进程中的数据的。在JVM中,Java堆和方法区是多个线程共享的数据区域。也就是说,多个线程可以操作保存在堆或者方法区中的同一个数据。那么,换句话说,保存在堆和方法区中的变量就是Java中的共享变量。
Java中哪些变量是存放在堆中,哪些变量是存放在方法区中,又有哪些变量是存放在栈中的呢?
Java中共有三种变量,分别是类变量、成员变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。
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的操作是不需要考虑线程安全的。
共享资源
同一进程中线程共享的资源到底有哪些?
线程的共性如下:
1、进程代码段;
2、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯);
3、进程打开的文件描述符;
4、信号的处理器;
5、进程的当前目录和进程用户ID与进程组ID。
除了上述线程的共性之外,线程当然还具有自己一些独立的资源。
线程的个性如下:
1、线程ID:每个线程都有自己的线程ID,这个ID在本进程中是唯一的,进程用此来标识线程。
2、寄存器组的值:由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有线程的寄存器集合的状态保存,以便将来该线程在被重新切换时能得以恢复。
3、线程的堆栈:堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。
4、错误返回码:由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了error值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。
5、线程的信号屏蔽码:由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。
6、线程的优先级:由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。