• 并发编程艺术-锁类型以及底层原理


    本文地址

    Java并发编程艺术-并发机制的底层原理实现

    1.Volatile

    定义:

    Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。

    volatile借助Java内存模型保证所有线程能够看到最新的值。(内存可见性)

    实现原理:

    将带有volatile变量操作的Java代码转换成汇编代码后,可以看到多了个lock前缀指令(X86平台CPU指令)。这个lock指令是关键,在多核处理器下实现两个重要操作:

    1.将当前处理器缓存行的数据写回到系统内存。

    2.这个写回内存的操作会使其他处理器里缓存该内存地址的数据失效

    如果了解计算机组成原理,可以知道CPU为了提高处理速度,不和内存直接进行交互,而是使用Cache(高速缓存,通过缓存数据交互速度和内存不是一个数量级,而同时Cache的存储容量也很小)。
    从内存将数据读到缓存后,CPU进行一系列数据操作,而操作完成时间是不可知的。而JVM对带有volatile变量进行写操作时,会发送Lock前缀指令,将数据从缓存行写入到内存。写入内存还不够,因为其他线程的缓存行中数据还是旧的,Lock指令可以让其他CPU通过监听在总线上的数据,检查自己的缓存数据是否过期,如果缓存行的地址和总线上的地址相同,则将缓存行失效,下次该线程对这个数据操作时,会重新从内存中读取,更新到缓存行。

    2.Synchronized

    Synchronized也是经常用到的,它给人的印象一般是"重量级锁"。在JDK1.6后,对Synchronized进行了一系列优化,引入了偏向锁和轻量级锁,对锁的存储结构和升级过程。有效减少获得锁和释放锁带来的性能消耗。

    Synchronized同步基础:

    1.普通同步方法,锁是当前实例对象。 public synchronized void test(){...}

    2.静态同步方法,锁是当前类的Class对象。public static synchronized void test(...){}

    3.对于同步方法块,锁是Synchronized括号中里配置的对象。synchronized(instance){...}

    用javap反编译class文件,可以看到Synchronized用的是monitorenter和monitorexit实现加锁。一个monitorenter必须要有monitorexit与之对应,所以同步方法会在异常处和方法返回处加入monitorexit指令。

             3: monitorenter  //注意此处,进入同步方法
             4: aload_0
             5: dup
             6: getfield      #2             // Field i:I
             9: iconst_1
            10: iadd
            11: putfield      #2            // Field i:I
            14: aload_1
            15: monitorexit   //注意此处,退出同步方法
    

    3.Java对象头

    Synchronized用到的锁存在Java对象头里,若对象非数组类型,用32bit存储(2个字宽,32虚拟机一个字宽为4字节,一个字节8bit)
    MarkWord存储和锁相关的信息:

    锁有四个等级: 无锁->偏向锁->轻量级锁->重量级锁。如果存在竞争,就会不断升级,但不会降级。

    1.偏向锁

    多数情况下,锁不会存在竞争,而是同一个线程多次获得。当某个线程访问同步块代码时,会将锁对象和栈帧中的锁记里存储锁偏向的线程ID,以后线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比对一下对象头中的MarkWord里的线程ID,如果一致则表示线程获得锁。若不一致,再继续测试偏向锁的标识是否为1:如果没有设置(无锁状态),用CAS(Compare and Swap)竞争锁;如果设置了,尝试使用CAS将对象头的偏向锁指向当前线程。

    当有另一个线程尝试竞争锁时,持有偏向锁的线程才会释放锁。需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

    Java 6,7默认开启偏向锁,可以通过JVM的参数-XX:-UsebiasedLocking=false关闭

    2.轻量级锁

    (1)加锁

    锁记录存储在栈桢,会将对象头的MarkWord复制到锁记录。线程在执行同步块时,会尝试用CAS将对象头的MarkWord替换为指向锁记录的指针,若成功,获得锁;失败表示其他线程竞争锁,当前线程尝试使用自旋获取锁。

    (2)解锁

    类似于加锁反向操作,会将锁记录复制会对象头的MarkWord。若成功,表示操作过程中没有竞争发生;若失败,存在竞争,锁会膨胀成重量级锁。

    如下图:

    当膨胀到重量级锁时,不会再通过自选获得锁(自旋时线程处于活动状态,会消耗CPU),而是将线程阻塞,获得锁的线程执行完后会释放重量级锁,此时唤醒因为锁阻塞的线程,进行新一轮的竞争。

    3.其他锁概念

    自旋锁:
    自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。

    public class SpinLock {
    
      private AtomicReference<Thread> sign =new AtomicReference<>();
    
      public void lock(){
        Thread current = Thread.currentThread();
        while(!sign .compareAndSet(null, current)){
        }
      }
    
      public void unlock (){
        Thread current = Thread.currentThread();
        sign .compareAndSet(current, null);
      }
    }
    

    使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。

    当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。

    由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。


    锁的优缺点对比:

    优点 缺点 适用场景
    偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
    轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求响应时间,锁占用时间很短
    重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,锁占用时间较长
  • 相关阅读:
    原生Js自适应浏览器宽度的瀑布流布局
    Fixed Gear DIY整车配色 JavaScript版制作过程
    DOM结构——两个节点之间可能存在哪些关系以及如何在节点之间任意移动
    盒模型 块级元素与行内元素 浮动元素
    Js 页面元素 简单震动效果
    CSS引入的方式有哪些? link和@import的区别?
    一道Web前端面试题 DIV放大
    js在IE和Firefox下不同之处
    Doctype? 严格模式与混杂模式如何触发这两种模式,区分它们有何意义?
    JavaScript document属性和方法
  • 原文地址:https://www.cnblogs.com/zhengshiqiang47/p/8686310.html
Copyright © 2020-2023  润新知