Java-基础知识-锁概念

本文印象笔记格式版本html

Java虚拟机对synchronized的优化java

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁,可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级.算法

 

偏向锁: 偏向锁是JDK 1.6以后加入的新锁,它是一种针对加锁操做的优化手段,通过研究发现,在大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,所以为了减小同一线程获取锁(会涉及到一些CAS操做,耗时)的代价而引入偏向锁。偏向锁的核心思想是,若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再作任何同步操做,即获取锁的过程,这样就省去了大量有关锁申请的操做,从而也就提供程序的性能。因此,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续屡次是同一个线程申请相同的锁。可是对于锁竞争比较激烈的场合,偏向锁就失效了,由于这样场合极有可能每次申请锁的线程都是不相同的,所以这种场合下不该该使用偏向锁,不然会得不偿失,须要注意的是,偏向锁失败后,并不会当即膨胀为重量级锁,而是先升级为轻量级锁数据库

 

轻量级锁:假若偏向锁失败,虚拟机并不会当即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6以后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁可以提高程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。须要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,若是存在同一时间访问同一锁的场合,就会致使轻量级锁膨胀为重量级锁编程

 

自旋锁: 轻量级锁失败后,虚拟机为了不线程真实地在操做系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数状况下,线程持有锁的时间都不会太长,若是直接挂起操做系统层面的线程可能会得不偿失,毕竟操做系统实现线程之间的切换时须要从用户态转换到核心态,这个状态之间的转换须要相对比较长的时间,时间成本相对较高,所以自旋锁会假设在不久未来,当前的线程能够得到锁,所以虚拟机会让当前想要获取锁的线程作几个空循环(这也是称为自旋的缘由),通常不会过久,多是50个循环或100循环,在通过若干次循环后,若是获得锁,就顺利进入临界区。若是还不能得到锁,那就会将线程在操做系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是能够提高效率的。最后没办法也就只能升级为重量级锁了。多线程

 

除此以外,锁消除也是一项很是重要的优化手段.Java虚拟机在JIT编译时(能够简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),经过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,经过这种方式消除没有必要的锁,能够节省毫无心义的请求锁时间.并发

 


原文地址app

 

 

1. 乐观锁 VS 悲观锁#性能

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不一样角度。在Java和数据库中都有此概念对应的实际应用。优化

先说概念。对于同一个数据的并发操做,悲观锁认为本身在使用数据的时候必定有别的线程来修改数据,所以在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

而乐观锁认为本身在使用数据时不会有别的线程修改数据,因此不会添加锁,只是在更新数据的时候去判断以前有没有别的线程更新了这个数据。若是这个数据没有被更新,当前线程将本身修改的数据成功写入。若是数据已经被其余线程更新,则根据不一样的实现方式执行不一样的操做(例如报错或者自动重试)。

乐观锁在Java中是经过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操做就经过CAS自旋实现的。

 

根据从上面的概念描述咱们能够发现:

  • 悲观锁适合写操做多的场景,先加锁能够保证写操做时数据正确。

  • 乐观锁适合读操做多的场景,不加锁的特色可以使其读操做的性能大幅提高。

 

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的状况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是经过CAS来实现了乐观锁。

 

CAS虽然很高效,可是它也存在三大问题,这里也简单说一下:

  • ABA问题。CAS须要在操做值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。可是若是内存值原来是A,后来变成了B,而后又变成了A,那么CAS进行检查时会发现值没有发生变化,可是其实是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

    • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操做封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,若是都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

  • 循环时间长开销大。CAS操做若是长时间不成功,会致使其一直自旋,给CPU带来很是大的开销。

  • 只能保证一个共享变量的原子操做。对一个共享变量执行操做时,CAS可以保证原子操做,可是对多个共享变量操做时,CAS是没法保证操做的原子性的。

    • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,能够把多个变量放在一个对象里来进行CAS操做。

 

 

2. 自旋锁 VS 适应性自旋锁#

在介绍自旋锁前,咱们须要介绍一些前提知识来帮助你们明白自旋锁的概念。

阻塞或唤醒一个Java线程须要操做系统切换CPU状态来完成,这种状态转换须要耗费处理器时间。若是同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。若是物理机器有多个处理器,可以让两个或以上的线程同时并行执行,咱们就可让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,咱们需让当前线程进行自旋,若是在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就能够没必要阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

 

自旋锁自己是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。若是锁被占用的时间很短,自旋等待的效果就会很是好。反之,若是锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。因此,自旋等待的时间必需要有必定的限度,若是自旋超过了限定次数(默认是10次,可使用-XX:PreBlockSpin来更改)没有成功得到锁,就应当挂起线程。

 

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,而且引入了自适应的自旋锁(适应性自旋锁)。

自适应意味着自旋的时间(次数)再也不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。若是在同一个锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也是颇有可能再次成功,进而它将容许自旋等待持续相对更长的时间。若是对于某个锁,自旋不多成功得到过,那在之后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

 

3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁#

 

4. 公平锁 VS 非公平锁#

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能得到锁。公平锁的优势是等待锁的线程不会饿死。缺点是总体吞吐效率相对非公平锁要低,等待队列中除第一个线程之外的全部线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但若是此时锁恰好可用,那么这个线程能够无需阻塞直接获取到锁,因此非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优势是能够减小唤起线程的开销,总体的吞吐效率高,由于线程有概率不阻塞直接得到锁,CPU没必要唤醒全部线程。

缺点是处于等待队列中的线程可能会饿死,或者等好久才会得到锁。

 

5. 可重入锁 VS 非可重入锁#

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会由于以前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优势是可必定程度避免死锁。

 

6. 独享锁 VS 共享锁#

独享锁和共享锁一样是一种概念。咱们先介绍一下具体的概念,而后经过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。若是线程T对数据A加上排它锁后,则其余线程不能再对A加任何类型的锁。得到排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。若是线程T对数据A加上共享锁后,则其余线程只能对A再加共享锁,不能加排它锁。得到共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是经过AQS来实现的,经过实现不一样的方法,来实现独享或者共享。