java并发编程 线程的可见性,有序性,一致性(二)

1.从共享数据说起(锁的转换)

 当我们需要操作一个数据,如果使用进程或者单线程,数据不存在任何问题,当使用多线程的时候,多个线程共享一条数据,就会存在线程数据的不一致问题,我们必须保护好共享数据,避免出现脏数据,保证线程的数据的一致性,从而使我们的数据能够完整的在线上运行。

 从synchronized 说起,它能够对线程加锁,保证数据的一致性,用法可以在下面的场景中
  • 修饰实例方法
  • 修饰静态方法
  • 修饰代码块

无论是哪种修饰,synchroized 总是依赖于对象的生命周期,那么对象中如何进行加锁操作的呢?

每一个对象都有一个对象头、实例数据和填充数据,对象头有age、锁标记、偏向锁等等,在内存中用markword(对象头)存储锁

1.6之前是基于重量级锁,为了提高性能,引入了偏向锁和轻量级锁

对象头记录锁的状态,通过它可以计算出锁的以下状态

  • 偏向锁
  • 轻量级锁
  • 重量级锁

那么锁之间的转换的原因是什么呢?

在两个线程交替访问的情况下,多个线程都是被同一个线程占用的前提下,引入了偏向锁,线程被某一个锁占用,另外的锁进入轻量级锁(两个线程交替访问),然后根据次数或者根据上一次释放锁的情况来进行控制自旋的次数。

偏向锁和轻量级锁都是属于无锁状态,不用进行用户态和系统态的切换,所以消耗系统的资源也比较少,因此,很有必要进行偏向锁和轻量级锁的升级,但是如果锁不能够长时间获取线程,就会导致系统消耗大量内存,从而影响了系统内存,因此,在一定的时间进入轻量级锁,如果线程还未获得锁,线程则要进入阻塞状态,即进入重量级锁。

举个例子,假设垃圾回收站收垃圾,A也卖垃圾,B也卖垃圾,A先到,A通过偏向锁,占用线程,B进入轻量级锁,B等待一段时间,等待A释放锁。

如果有很多人,前一个人垃圾太多,导致占用时间过长,这个时候,剩余的人只能在休息室等待,就是所谓的重量级锁。 

ps:线程要基于数据安全性和提高性能的原则来处理数据。

 2. 从提高数据运行的效率说起

计算的发展从原来的单核心变成了多核心,计算机的性能也随之提高,但是计算机的性能会受到最小的那一部分,当然cpu的效率最高,并且有多核,这里制约计算机性能的主要是内存,于是计算机底层出现了数据总线锁,即每个数据在从内存到总线的时候都是要经过数据总线,然后修改完成之后就会将锁释放,这样是一个同步的操作,无法发挥多个cpu的效率,于是就出现了缓存锁.

缓存锁就是通过mesi机制控制数据,每一块cpu内都有缓存区域,通过mesi状态来控制数据的生效和失效,这里的控制是异步的,能够大大的提高计算机的性能.

MESI表示数据的几种状态,每一块cpu的数据都是通过嗅探数据来确定自身的状态.他们的状态如下.

M:被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
E:独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。
 S:共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
 I:无效的。本CPU中的这份缓存已经无效。

但是这种缓存锁也无法完全保证数据的安全性,在数据未更新的空隙,也会出现脏数据的问题,即出现执行重排序。

那么jmm如何保证数据的在各个cpu中的安全性呢?

jmm引入了内存屏障,在增删改查操作的时候,提供了相应的隔离,从而保证了数据的安全性.保证了线程的原子性 ,可见性和一致性

3.原子性(Atomicty)

原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

synchronized 在对表头进行锁定,通过字节码编译后会出现monitorenter 和monitorexit 对代码片段进行锁定,从而保证了线程的原子性操作,
volatile 不能保证线程的原子性.(对单线程是可以保证原子性,但是多线程就不能了,因为volatile的内存屏障是保证从主内存的数据读取的数据是一致的,不能保证操作的独立性,
如果两个线程同时修改一个数据,无法保证线程的原子性,比如i++操作,分为三步,获取,加1,写入,如果在获取的时候被阻塞,第二个线程再次进行操作,获取的操作则变成了脏数据。)

lock可以保证线程的原子性

4.可见性(Visibility)

可见性是指当一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改。即对其他的线程可见。可见性问题在串行程序中来说是不存在的,但是在并行的程序中是存在的。

java内存模型是从线程->工作内存->主内存,如果一个线程修改了值,则另外一个线程必须知道这个值修改了,才能保证线程的可见性.

synchronized 通过一个线程访问的时候,另外一个线程不能够访问这个线程,即对线程的锁定,从而保证了线程的可见性。

volatile通过内存屏障 (load store)等指令,即一个线程读之前要保证另外一个线程写完,从而保证了线程的可见性。

5.有序性(Ordering)

对于同一个线程的执行代码而言,我们总是习惯性的认为代码是从前往后依次执行。但是在并发的时候程序的执行可能会出现乱序。直观感觉是写在前面的代码,会在后面执行。

简单的说就是从虚拟机和硬件为了提高性能,将一些快的程序先运行,其他的比如需要分配内存空间等进行等待,就是对程序的指令进行重排序,比如同时洗衣机洗衣服,做饭,先打印做饭,然后打印洗衣服,但是洗衣服中间可以做饭,所以程序先执行了洗衣服,然后再执行做饭。

虽然顺序不一致,但是程序遵守以下happen-before原则

指令重排序的规则 Happen-Before规则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量先于读发生,这保证了volatile的可见性
  • 锁规则:解锁必然发生在随后的加锁前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start()方法先于他的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行、结束先于finalize()方法.
  • final语句

猜你喜欢

转载自www.cnblogs.com/speeding360/p/11193811.html