线程安全

线程安全(常考)

  1. 线程安全 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。

    1. 线程不安全 就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据不是期望的数据

    通俗解释:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    好比你有两个一模一样的银行卡(账户一样,余额一样,当然现实中是没有的),假如卡上余额1000块,而你跟你女朋友同时在不同的ATM上面取1000块钱(是同时哦,理想中的同时),如果线程不安全,那么俩人都能同时取出1000块(赚死了)。而如果线程安全的话,只能一个人同时操作一个账户,当这个账户正在被操作时,是被锁起来的,不给别人动的,只能你自己动,你动完了别人才能动。哈哈哈哈,有趣的例子

为什么会有线程安全问题?
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

安全性的举例说明:
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了

线程(Thread),有时被称为轻量级进程(LWP),是程序执行流的最小单位;一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成通常情况下,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间及一些进程级的资源。

在大多数软件应用中,线程的数量都不止一个,多线程程序处在一个多变的环境中,可访问的全局变量和堆数据随时都可能被其他的线程改变,这就将“线程安全”的问题提上了议程。那么,如何确保线程的安全呢?
线程安全
一般说来,确保线程安全的方法有这几个:竞争与原子操作、同步与锁、可重入、过度优化

竞争与原子操作
多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的(Atomic),因为不管怎样,单条指令的执行是不会被打断的。

因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就要选用其他的方法了。

同步与锁的搭配使用
为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。

同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。

二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称信号量)。

可重入
一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。

过度优化
在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。

我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整操作volatile变量的指令顺序。

在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。

对barrier的认识:
barrier是保证指令执行顺序的
举个例子来说:比如在一个核上执行, a = 3 ; b = 2; flag = 1;
在另一个核上,你判断 if(flag == 1) { c = a + b;}
目的是想等a和b的值赋值成功后通过flag进行标志,但是在编译器优化后,可能在a和b尚未赋值的时候,flag的值已经赋值为1,这个时候另一核上的逻辑就是错的。
对于这样的情况,就需要在a、b的赋值语句与flag的赋值语句之间增加barrier,内存屏障,从而保证顺序。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值