作者: Ary_zz | 来源:发表于2020-01-19 15:22 被阅读0次

    2019-09-04

    volatile

    lock前缀的指令在多核处理器下会将当前处理器缓存行的数据会写回到系统内存,这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效

    乐观锁

    读多写少,CAS实现
    遇到并发写的可能性低
    一般会使用版本号机制或CAS算法实现

    悲观锁

    写多
    遇到并发写的可能性高
    每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁
    java的悲观锁 - Synchronized
    AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock

    阻塞的代价

    java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

    • 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
    • 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

    synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

    markword

    markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态

    自旋锁

    自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

    但是线程自旋是需要消耗cup的,说白了就是让cpu在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。

    如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

    但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;

    自旋锁与互斥锁都是为了实现保护资源共享的机制。
    无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
    获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

    自旋锁:

    • 线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
    • 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
    • 自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
    • 自旋锁本身无法保证公平性,同时也无法保证可重入性。
    • 基于自旋锁,可以实现具备公平性和可重入性质的锁。

    单核cpu下时间片调度,一定会有进程上下文切换的,自旋锁在单核情况下会一直把时间片占满再被调度,还不如普通锁。

    JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化

    Synchronized

    把任意一个非NULL的对象当作锁

    • 作用于方法时,锁住的是对象的实例(this);
    • 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
    • synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

    Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

    synchronized通过对象的对象头(markword)来实现锁机制,java每个对象都有对象头,都可以为synchronized实现提供基础,都可以作为锁对象,在字节码层面synchronized块是通过插入monitorenter monitorexit完成同步的。持有monitor对象,通过进入、退出这个Monitor对象来实现锁机制。

    synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。

    在Java内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。

    Reentrylock

    • 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况
    • 一个互斥的同步器,在多线程高竞争条件下,ReentrantLock比synchronized有更加优异的性能表现
    • Lock使用起来比较灵活,但是必须有释放锁的配合动作
      Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
      Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等
    • 能被中断地获取锁的特性:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
    • 在指定的时间范围内获取锁;如果截止时间到了仍然无法获取锁,则返回
    • 提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作
    • ReentrantLock可实现公平锁和非公平锁,公平锁就是先来的先获取资源。
    • ReentrantReadWriteLock用于读多写少的场合,且读不需要互斥场景

    偏向锁

    偏向锁的适用场景
    始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
    在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;
    很多但又很短的安全点,全都是RevokeBias, 高并发的应用会禁用掉偏向锁。

    轻量级锁

    偏向锁升级而来
    偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
    如果多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

    锁优化

    • 减少锁的时间
      不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;

    • 减少锁的粒度
      它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;

    java中很多数据结构都是采用这种方法提高并发操作的效率:

    • ConcurrentHashMap
      java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组

    Segment< K,V >[] segments
    Segment继承自ReenTrantLock,所以每个Segment就是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。

    • LongAdder
      LongAdder 实现思路也类似ConcurrentHashMap,LongAdder有一个根据当前并发状况动态改变的Cell数组,Cell对象里面有一个long类型的value用来存储值;
      开始没有并发争用的时候或者是cells数组正在初始化的时候,会使用cas来将值累加到成员变量的base上,在并发争用的情况下,LongAdder会初始化cells数组,在Cell数组中选定一个Cell加锁,数组有多少个cell,就允许同时有多少线程进行修改,最后将数组中每个Cell中的value相加,在加上base的值,就是最终的值;cell数组还能根据当前线程争用情况进行扩容,初始长度为2,每次扩容会增长一倍,直到扩容到大于等于cpu数量就不再扩容,这也就是为什么LongAdder比cas和AtomicInteger效率要高的原因,后面两者都是volatile+cas实现的,他们的竞争维度是1,LongAdder的竞争维度为“Cell个数+1”为什么要+1?因为它还有一个base,如果竞争不到锁还会尝试将数值加到base上;

    • LinkedBlockingQueue
      LinkedBlockingQueue也体现了这样的思想,在队列头入队,在队列尾出队,入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高;

    拆锁的粒度不能无限拆,最多可以将一个锁拆为当前cup数量个锁即可;

    • 锁粗化
      大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;
      在以下场景下需要粗化锁的粒度:
      假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;

    • 使用读写锁
      ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;

    • 读写分离
      CopyOnWriteArrayList 、CopyOnWriteArraySet
      CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
       CopyOnWrite并发容器用于读多写少的并发场景,因为,读的时候没有锁,但是对其进行更改的时候是会加锁的,否则会导致多个线程同时复制出多个副本,各自修改各自的;

    • 使用cas
      如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择

    Atomic类操作
    数据库乐观锁更新

    ReentranLock

    基于AQS,AbstractQueuedSynchronizer
    可以是公平锁,支持中断和超时等待
    同步队列,是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁。
    条件队列,是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。

    读锁和写锁都是可重入锁
    可以支持锁降级
    如果一个线程持有了写锁则可以再次持有读锁然后释放写锁
    如果一个线程持有了读锁,则不能当前线程和其它线程都不能再持有写锁

    synchronized 和 lock

    https://cloud.tencent.com/developer/article/1495313
    Synchronzied 和 Lock 的主要区别如下:

    存在层面:Syncronized 是Java 中的一个关键字,存在于 JVM 层面,Lock 是 Java 中的一个接口
    锁的释放条件:1. 获取锁的线程执行完同步代码后,自动释放;2. 线程发生异常时,JVM会让线程释放锁;Lock 必须在 finally 关键字中释放锁,不然容易造成线程死锁
    锁的获取: 在 Syncronized 中,假设线程 A 获得锁,B 线程等待。如果 A 发生阻塞,那么 B 会一直等待。在 Lock 中,会分情况而定,Lock 中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待
    锁的状态:Synchronized 无法判断锁的状态,Lock 则可以判断
    锁的类型:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是 可重入,可判断,可公平锁
    锁的性能:Synchronized 适用于少量同步的情况下,性能开销比较大。Lock 锁适用于大量同步阶段:
    Lock 锁可以提高多个线程进行读的效率(使用 readWriteLock)
    在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
    ReetrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等

    https://www.jianshu.com/p/09d5ba4bfb7a

    可重入

    线程可以进入任何一个它已经拥有的锁所同步着的代码块。

    相关文章

      网友评论

          本文标题:

          本文链接:https://www.haomeiwen.com/subject/fxdlyctx.html