锁的优化(偏向锁、轻量级锁、重量级锁,CAS)

为什么出现CAS?

我们知道synchronized关键字不论是实现同步方法还是同步代码块,都需要首先执行monitorenter获取到锁对象的监视器monitor,同一时刻只有一个线程可以获取到锁对象的monitor监听器,退出时需要执行monitorexit进行解锁。
monitor机制是JDK1.6之前synchronized(内建锁)底层原理,又称为JDK1.6重量级锁。线程的阻塞和唤醒均需要操作系统由用户态切换到内核态,开销十分大,因此效率很低。并且是JVM层面锁。

基于上面原因,在JDK1.6对内建锁有了以下优化:自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。
锁优化后,并不是不需要等待,只是等待时间变短

在理解锁优化之前先了解2个概念:CAS操作和Java对象头。

什么是CAS(Compare and Swap)?
在介绍CAS前,先简单看一下乐观锁和悲观锁。

悲观锁:JDK1.6以前线程获取内建锁(synchronized)是一种悲观锁策略。
悲观锁顾名思义就是运气很差,只要我需要锁,就有其他线程和我争抢锁~~
每一次访问临界资源都会发生冲突,如果我费九牛二虎之力获取到锁后,我获取到锁的同时会阻塞其他未获取到锁线程。

乐观锁:乐观锁是所有线程访问共享资源都不会发生冲突,既然不会发生冲突就不会阻塞其他线程,那么线程不会出现阻塞等待的状态。
每次操作都认为不会发生冲突,尝试执行,并检测结果是否正确。如果正确则执行成功,否则发生了冲突,回退再重新尝试直到没有冲突。其实重试时间很快,差不多几毫秒,这个线程在尝试的过程一直在cpu上跑,只是跑的是无用指令,那么就没有从用户态切换到内核态,开销因此变小。

在Java多线程中,乐观锁一个常见实现是CAS操作。

CAS操作过程:
CAS(比较交换)。
其具有三个操作数:

  • V:当前内存地址实际存放的值;
  • O:内存存放预期值(旧值);
  • N :更新的新值。

当V=0,预期值和内存实际值相等,内存存放的实际值没有被任何其他线程修改,即O就是目前最新的值,可以将新值N赋给V;

当V!=O,表明内存存放的实际值已经被其他线程修改,因此O值不是当前最新值,返回V,无法修改。(将V返回是因为再次重试时期待值就变为V)

可以将上述情况简单举一个例子:
比如说去餐厅买饭,正在买饭的同学是小米,我期望的人也是小米,小米买完就轮到我买,可以这个时候我去餐桌放书包,回来发现买饭的同学不是小米而是小明,也就是说预期值和实际值不同,那么就轮不到我买饭~~但是我会知道现在实际买饭的同学是小米,那我在等会,小明买完就是我买 ~ ~
如果回来小米刚好买完,我就可以买饭,也就是说现在实际买饭的人更新为我。开心,终于可以买饭了~ ~

当多个线程使用CAS操作时一个变量时,只有一个线程会成功,并成功更新,其余线程均会失败。失败的线程会重新尝试(自旋),当然也可以选择重新挂起(阻塞)。

内建锁和自旋锁区别:
没有优化的synchronized(内建锁)的主要问题是:在存在线程竞争的情况下会出现线程的阻塞和唤醒带来的性能问题,这是一种互斥同步(阻塞同步);
而CAS操作失败后,并不是武断的将线程挂起,失败后会进行一定的尝试,而非耗时的将线程挂起,因此CAS也称为非阻塞同步。

CAS存在问题
1.ABA问题:
比如一个线程1从内存位置V取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作变成B,然后线程2又将内存位置V的数据变为A,这时候线程1进行CAS操作发现内存中仍然是A,然后线程1操作成功。尽管线程1的CAS操作成功,但是并不代表这个过程没有问题。如果链表的头在变化了2次后恢复了原值,但是并不代表链表没有变化。
对于ABA问题解决办法是添加一个版本号,对上述例子是将2个A进行不同的标记比如A1,A2等,由于版本号不同,线程1无法操作成功。在JDK1.5之后使用atomic包中提供的AtomicStampedReference 类来解决。

2.自旋会浪费大量的CPU资源:
与阻塞线程相比,自旋会浪费大量的处理器资源。因为当前线程仍处于运行状态,只不过跑的是无用指令。自旋线程希望在运行无用指令时,锁能够被释放出来。

解决办法是:自适应自旋:根据以往自旋等待时间能否获取锁来动态调整自旋的时间(循环次数)。
如果在上次自旋时获取到锁,则会稍微增加下一次自旋时间;否则即稍微减少下一次自旋时间。

简单举一个例子:
比如在红绿灯时:当碰到红灯时,有两种措施,一个是熄火停车,一个是将离合踩到底并且踩刹车即怠速停车,如果怠速停车时等到 绿灯,下一次碰到红灯就会继续怠速停车并且等待时间会长一点,如果之前怠速停车没碰到绿灯,那么下一次碰到红灯怠速时间会短一点,意思是怠速一会没等到绿灯就会熄火等待。
在这个例子里,Java线程获取锁相当于碰到红灯等待绿灯,而线程阻塞相当于熄火停车,而线程自旋相当于怠速停车。JVM根据上一次自旋等待时间来改变下一次自旋等待时间。即如果上次自旋时获取到锁,则会稍微增加下一次自旋时间;否则即稍微减少下一次自旋时间。

3.公平性:
处于阻塞状态的线程无法立刻竞争被释放的锁,然后,处于自旋状态的锁很有可能优先获取锁。
比如说:小米和小明中午在餐厅买饭,由于人很多,一时半会吃不到饭,小米选择在餐桌上坐会儿来看下买饭人数,而小明选择一直坐在餐桌,有一次小米查看时发现人不多就买了饭,而小明再次来的时候发现人更多。
小米的做法就相当于自旋—>优先获取到锁;
小明的做法相当于阻塞等待—>无法立刻竞争被释放的锁。

Java对象头

在线程同步时获取对象的monitor,就是获取到对象的锁,怎么就获取到锁了呢?或者这个锁是什么东西呢?锁是对对象的一个标记,标记表明对象当前锁的状态,线程应该如何竞争锁,这个标记存放在Java对象的对象头。Java对象头里的Mark Word里默认存放的是对象的Hashcode,分代年龄和锁标志位。
32位JVM 对象头默认存储结构:
图(对象头)

在JavaSE 1.6中,锁一共有4种状态,分别是无锁,偏向锁,轻量级锁,重量级锁,并且这4种锁的的级别依次升高,锁可以升级但是不能降级,比如可以将偏向锁升级为轻量级锁,轻量级锁升级为重量级锁。
不能降级是因为:由于有多个线程竞争锁才升级锁,如果发生降级还要再升级,没有意义,而且效率会降低。
对4种状态的MarkWord变化为:

在这里插入图片描述
注意:这四种状态并不是Java语言中的锁,而是JVM为了提高锁的获取与释放而做的优化(使用synchronized 时)
首先通过一个例子来解释下偏向锁、轻量级锁、重量级锁的区别:

假如家里只有一个碗,当我一个人在家时,没有人会和我争抢这个碗。从始至终只有我用这个碗,这时为偏向锁状态。
当我和妹妹2个人在家时,妹妹饿了需要用碗来泡面,而我不是很饿,就会等妹妹吃完我再用这个碗,我俩在不同时刻用这个碗,这时为轻量级锁。
当我和妹妹都很饿时,都需要用这个唯一的碗来泡面,于是就会发生竞争这个碗(没办法,我也很饿 ~ ~),也就是说在同一时刻竞争同一共享资源,这时是重量级锁

偏向锁

偏向锁的由来:
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁是四种状态中最乐观的一种锁:从始至终只有一个线程请求某一把锁。
偏向锁头部Epoch字段值:表示此对象偏向锁的撤销次数。当默认撤销次数是40以上,表示此对象不再适用于偏向锁,当下次线程再次获取此对象时,直接变为轻量级锁。

偏向锁的获取:

1.当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退
出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着当前线程id。
2.如果测试成功,表示线程已经获得了锁,不用CAS,直接执行同步体;如果测试失败,则需要再测试一下Mark Word中偏向锁的标
识是否设置成1(表示当前是偏向锁)。
3.如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象
头的偏向锁指向当前线程。

偏向锁的撤销:
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放
锁。

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

在这里插入图片描述

关闭偏向锁:
偏向锁在JDK6之后是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭
延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过
JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
偏向锁只有一次CAS过程,就是第一次进入同步代码块,之后进入只会比较对象头存的线程id是否是要访问线程id。

轻量级锁:
轻量级锁:多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。

轻量级锁的加锁:

1.线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中(是为了撤销),称为Displaced Mark Word,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。
2.如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋失败,会升级为重量级锁。

轻量级锁的解锁:
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。
如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
在这里插入图片描述

轻量级锁和偏向锁的比较:
1.偏向锁只会CAS一次,第一次进入时;而轻量级锁不停的CAS;
2.在偏向锁中,线程栈帧中复制的对象头Mark Word里的线程id;而在轻量级锁中,线程栈帧中复制的对象头Mark Word中所有信息。
3.当线程访问同步代码块失败时,使用CAS竞争锁,并将偏向锁升级为轻量级锁。(升级前,会先将偏向锁撤销为无锁,再从无锁到轻量级锁)
4.当其他线程竞争偏向锁时,会有撤销为无锁和升级轻量级锁之分;而当其他线程竞争轻量级锁时,会直接升级为重量级锁。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
多个线程在同一时间竞争同一把锁会升级为重量级锁。

重量级锁

当其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁后才会唤醒这些线程来进行竞争。

3种锁总结:
Java虚拟机中synchronized关键字的实现锁,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。

  1. 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋(如果获取到锁就返回,失败就阻塞),来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
  2. 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
  3. 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。

锁的其他优化:
锁粗化:
锁粗化是将多次连接在一起的加锁解锁操作合并为一次操作。将多个连续的锁扩展为一个更大的锁。
比如下列的调用append方法,由于append是同步方法,即每次调用都需要加锁解锁操作,但是编译器检测到有连续的加锁解锁就会在第一个append处加锁,在最后一个append解锁:

//append是同步方法
 public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
//锁粗化
package CODE.多线程;

public class Cuhua
{
	static StringBuffer str=new StringBuffer(); 
    public static void main(String[] args) {
        str.append("a");
        str.append("b");
        str.append("c");
        System.out.println(str); //abc
    }
}

锁解除:
锁解除:删除不必要的加锁操作。根据代码逃逸技术,如果判断一段代码中,堆上的数据不会逃逸出当前线程,也就是每个线程都会堆上的数据,则认为此代码是线程安全的,无需加锁。但是对于锁粗化的代码中str,str是对象属性,即一个对象只有一个str,多线程就只有这一个str,即共享资源。

public class Cuhua
{
    public static void main(String[] args) {
    	//str是堆上数据
        StringBuffer str=new StringBuffer();
        str.append("a").append("b");
        System.out.println(str);
    }
}

猜你喜欢

转载自blog.csdn.net/sophia__yu/article/details/84104542