c# 数据出现错误! 线程间操作无效: 从不是创建控件_多线程的安全问题以及对volatile关键字的理解...

8d978222fe3f9b133dc5183a58cc5e97.png

上一篇我们说到线程并发与并行在于我们看来都是多线程,多线程之间它们会共享当前进程的资源,在共享的过程中,会出现一系列的问题,如数据“脏读,死锁等问题。线程的原子性,有序性,可见性以及volatile关键字和synchronized。

Java内存模型

d7e81710dba8d8fba73a00e64a74ed10.png

从上图我们知道Java内存模型规定了所有的变量都存储在主内存中。每条线程中有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

在这里就出现了一些问题,线程A改变了变量i,那么线程B会及时的获取到吗?那么A改变了i,会及时的写入主内存中吗?要回答这些问题,我们首先要了解几个概念:原子性,有序性,可见性。

原子性
1.定义

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

2.例子
就说银行转账的例子,A给B转1000,那么分这几步,从账户A减去1000元,往账户B加上1000元。试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

3.java的原子性
在Java编程过程中。怎么样的表达式才有原子性,那怎么样才能保证原子性呢。平常来说就一次操作的都有原子性,多次操作的我们可以用锁保证原子性(synchronized和Lock来实现)
如:

x 

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,从而保证了原子性。

1d0ba3087b02c661358e975157c2a876.png


在以上红色选中的三个部分,线程都有可能进行切换,而A线程进来了,突然到B进程,那么B进程改变了num=0;按道理来说已经没有票了,但是到A时,他已经进来了,就输出买票了。如果在一个线程进入到if中之后,当cpu切换到其他线程上时,不让其他的线程进入if语句,那么就算线程继续执行当前其他的线程,也无法进入到if中,这样就不会造成错误数据的出现。于是出现了用锁保证原子性(synchronized和Lock来实现)

synchronized

可见性
当然只一个原子性,也是不能保证数据的正确,还要有可见性。
1.定义
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
2.例子
还是上面的例子,如果synchronized这个只有原子性的话,那么保证了同一时刻只有一个线程执行,假如num=1,但是A线程改变了num=0,没有写入到主内存中,B线程获取到的数据还是1,也会有数据问题。所以通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。但是本身就是原子性的表达式,我们可以用volatile关键字修饰,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
如:

//线程1

很多人在中断线程时可能都会采用这种标记办法,但是这样会出现问题的,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。

注意:volatile不能保证原子性,但是又可见性和有序性。

volatile的应用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中。
例子:
1.状态标记量

volatile 

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

//线程1:

可能语句2会在语句1之前执行,那么就有可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值