欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

锁的运用优化和重排序

程序员文章站 2022-06-28 17:21:04
零蚀前言内容前面基本已经涵盖了锁的应用和一小部分原理,但是讲解的也不是很细致,并没有包含自旋,轻量级锁,偏向锁的方向进行阐述内容,但是并不会从源码的层次进行深究,只是语言阐述一下为什么要有这些东西,为什么会这样而已。步骤:step 1 :锁优化step 2 :重排序 & CAS专有名词解释可能遇到的专有名词串行(Sequential):串行的定义就是在同步性,所有的事件共用一个“通道”,导致了,一次只能有一个事件被执行通过。并发(Con....


  • 内容

    •  本文向自旋,轻量级锁,偏向锁的方向进行阐述内容,本篇并不会从源码的层次进行深究,只是语言阐述一下为什么要有这些东西,为什么会这样而已。

    • 步骤:

      • step 1 :锁优化
      • step 2 :重排序 & CAS

专有名词解释

  • 可能遇到的专有名词

    • 串行(Sequential):串行的定义就是在同步性,所有的事件共用一个“通道”,导致了,一次只能有一个事件被执行通过(并且是前一个事件结束后才会执行后一个事件)。

    • 并发(Concurrent):并发强调的是同时发生,但不代表同时运行,这怎么理解,就像串行一样,可能只有一个通道,但是串行,是每个事件执行完才到下一个进“通道”,而并发可能是多个事件在管道排队,但是每个事件我都执行一点然后执行其他事件,这样一点点的把事情都执行完,虽然用时和串行一样(假设所有的事件没有空闲事件),但是看起来好像一起执行完的(时间片理论)。

    • 并行(Parallel):并行,如名字一样,一个通道改多个,让多个事件一起通过,谁也不等谁,这就是并行。

    • 线程的安全性:线程安全的主要的原因是由于线程的竞态导致的,如果线程间没有竞态则不会有线程安全。(竞态:争抢CPU资源的状态,脏读也是在竞态下发生的)。

    • 原子性(Atomic): 原子性又叫不可分割性。(java中除了long和double其他基本类型都是原子的,这是由jvm规范,例如long 赋值-1 or 0 ,它有高位32,和低位32取值)

    • CAS(Compare-and-Swap): CAS更像是一种“硬件锁”,他是在处理器和内存这一层面上进行实现原子性。

    • volital:jvm虚拟机对这个修饰符主要是放在可见性上,但是他也有一定的原子性,他可以让long和double这两个类型在写操作上具有原子性,除此之外并没有什么原子性,可见性可看【No.4 Lock & Time & 单例】。

    • 上下文切换: 当一个线程的时间片由于某种原因*放弃,而给予另一个线程拥有着时间片的过程叫做上下文切换,前者为切出,后者为切入。上下文的切换会导致虚拟机的大量内存开销。


锁优化

  • 自旋

    • 说到自旋,可能很多人不清楚这是怎么回事,其实这个也很简单,就是一个while循环,当一个机器有多核处理,并且有多线程并行的情况下,后面请求锁的线程,我们会让它等一会,但不放弃对CPU的占有(毕竟是多核并行),然后看看前面那位占有锁的线程是否快要释放锁。这时候为了让这个等一会的线程不会被挂起,我们通常会让它处于忙循环的状态,也就是自旋(这里也就是一个“while ture”的循环),这个技术也就是自旋锁。

    • 为什么需要自旋锁这个机制呢,首先我们要知道一点,一个线程从阻塞到被挂起,从挂起到唤醒,都是在用户态和核心态,核心态到用户态的一种切换,这种切换是非常耗损性能的,所以宁可通过死循环来消耗,也不会选择切换状态,这是对性能消耗的考量。

    • 在短时间的锁占用的情况,自旋效果是非常好的,但是如果针对长时间的锁占用,那么自旋也是一个非常消耗性能的行为。所以默认规定了如果自旋10次还没能获取到锁,那么线程就不再执着了,去自挂东南枝了。

  • 自适应自旋

    • 上面说到的自旋默认是10次,但是这个太过于死板,如果第一次9次就成功了,如果之前那个线程又获得了锁,很可能他会要自旋11次才成功,因为它的事件的大小很可能会略大于自旋10次的时间,所以在JDK1.6上,如果是同一个线程,之前自旋成功了,那么虚拟机觉得这次应该也可以自旋成功,他会稍微放大自旋的次数。如果对于一个线程虚拟机一直自旋失败,那么虚拟机可能直接跳过自旋过程,自动挂起,这就是虚拟机对“老赖线程”的处理机制,JVM也越来越机智。
  • 锁消除&锁粗化

    • 锁消除,是针对一些类似于逃逸分析的情况来说的,它是看看那这个锁內的操作,是其他的线程不回来争抢,则他会取消锁,并且把这部分的操作当作线程里面私有的操作,比如说像final修饰的类,String等。

    • 锁粗化,锁粗化是针对频繁调用锁,但是我增大临界范围接可以避免这种情况,所以才有了锁粗化,这里的粗,是指将临界边缘粗大化,如下代码,粗化前可能循环很多次锁,这是非常消耗性能的。

    // 粗化前 while(true){ synchronized(this){ ...... } } // 粗化后 synchronized(this){ while(true){ ...... } } 
  • 轻量级锁

    • 轻量级锁,相对于重量级锁,重量级锁的理念是互斥,互斥才有同步,但是操作互斥就会引发一系列的阻塞,挂起,等耗损资源的操作,所以在重量级锁之后又推出了轻量级锁,轻量级是一种状态,不是某一个特定的锁,只要有互斥就是重量级,只要避免互斥就是轻量级。

    • 在对象里都会有一个字段Mark word来记录,这个对象持有轻量级锁,重量级锁还是无锁,当一个对象处于无锁状态时候他的标识位为01,当这个对象持有了锁,并且没有其他的线程来挣这个锁,那么他就是轻量级锁。在此之前,JVM会建立一个lock record的空间在线程的栈内,CAS对存储对象Mark word拷贝至lock record,如果这个存储拷贝的情况成功,那么状态且标示位改为00,如果操作失败说明不止一条线程争抢这个锁,如果多个线程抢夺一个锁,那么锁就会膨胀为重量级锁,标示位变为10

    • 偏向锁,偏向锁是对轻量级锁的进一步升华,轻量级锁是通过CAS操作来做的,也就是状态位为01,偏向位为1,如果该锁的线程一直没有被另一个线程争夺锁,持有该锁的线程将不需要再同步。一旦如果有一个线程出来尝试争抢锁,那么对象立刻将偏向锁状态改为,轻量级锁或者重量级锁。

  • 再谈谈volatile

    • 我们之前有说过volatile是在写操作上是有原子性,而且针对的是double和long这种没有原子性的变量声明。而这种原子性的操作是不会引发上下文的切换的,所以不会引发大量的内存开销,所以volatile的内存开销会小于synchronized。

    • volatile主要实现的功能在于可见性,写操作的原子性。它不能代替sychronized,他只是减小开销,和在一些场景下可能比较适用。


重排序 & CAS

  • 重排序解释

    • 执行操作的时候,为了提高编译器和处理器的执行的能力,编译器和处理器会进行重排序,编译器优化的重排序是在编译器完成的,指令重排序和内存从排序是处理器重排序。

    • 编译器重排序是不改变但线程语意的情况下重新安排语句的执行顺序,指令集重排序序,是不存在数据依赖的情况下改变机器指令的顺序,内存系统重排序,是使用了读写缓存区。

    • 重排序可能会导致多线程对某些属性的可见性发生改变(例:由于先后顺序的改变可能导致数据的某些变化,这很类似于我们的脏读),程序的读写顺序也可能和内存的读写顺序不一样。

    • as-if-serial语义,指不管怎么排序,结果不能变,也就是不会对存在依赖关系的操作进行重排序,happens-before语义,保证了线程的内存可见性。

    • 而在final,synchronized,volatile是可以防止这种重排序的产生。

  • CAS

    • 上面好像简单的提过CAS,它是Compare and Swap的简称,是一种处理指令集的称呼,是偏向物理层面处理同步问题的方案,其实用过Atomicxxxx做原子化操作的人,可能会用到CAS这个工具,以AtomicInteger为例,我们来看看他底层源码,你不难发现它里面用了一个Unsafe的对象,然后我慢看看这个对象用到的方法,可以很清楚的看见,它使用了CompareAndSwap方法。
     public final native boolean compareAndSwapObject(java.lang.Object o, long l, java.lang.Object o1, java.lang.Object o2); public final native boolean compareAndSwapInt(java.lang.Object o, long l, int i, int i1); public final native boolean compareAndSwapLong(java.lang.Object o, long l, long l1, long l2); 
    • 这些都是本地方法,也就是调用了底层的CAS方式,来进行同步设置。那么我们什么时候会用到这种方式呢,我们看下面代码,我们如果使用一个synchronized来对一个自增操作进行同步处理,真的有点杀鸡焉用牛刀的感觉。所以Atomic“Number”就应运而生了。
    synchronized(this){ i++; } 
    • CAS的处理方式类似于一种代理方式,当值的变化,和读取需要通过这个代理才行,而这个代理,会对这个值进行判断,如果有线程来写入数据,但是CAS判断这个值已经发生了变化,则这个线程的读取值和新值不一致,对导致写失败,而线程会选择再次尝试,直到写入成功。(CAS是一个if-then-act操作)
    boolean CompareAndSwap(Variable V,Object A,Object B){ if(A==V.get()){ // 检查变量是否被其他线程修改过 V.set(B); // 更新变量 return true; // 更新成功 } return false; // 已经被其他线程改变 }