CAS操作、Java对象头、偏向锁的获取与撤销、轻量级锁的获取与撤销、锁粗化、锁消除


Synchronized优化:
  在JDK 1.5中,synchronized的性能是很低的,因为这是一个重量级的操作,它对性能最大的影响是阻塞的实现、挂起线程、和恢复线程都需要转入内核态完成,这些给操作系统的并发性带来了很大的压力。
  但是在JDK1.6,发生了变化,对synchronized加入了很多优化措施,
  之前我们已经对synchronized有一定的了解,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而2进入到同步代码块或者同步方法中,即表现为互斥性(排它性)。这种方式效率低下,每次只能通过一个线程,既然每次都只能通过一个,如果这种形式不能改变的话,那么我们能不能让每次通过的速度变快一点呢?
  比如:去收银台付款,之前的方式是大家都去排队,然后取纸币付款收银员找零,付款的时候需要在包里拿出钱包再拿出钱,这个过程是比较耗时的,然后支付宝解放了大家去钱包找钱的过程,现在只需要扫描二维码就可以完成付款,也省去了收银员找零的时间,同样是需要排队付款,但整个付款的时间大大缩短,整体效率也变快了。这种优化同样可以引申到锁优化上,缩短获取锁的时间。
在锁优化之前,需要关注两个知识点(1)CAS操作 (2)Java对象头

1. CAS操作

1.1 什么是CAS?

  在使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态,那么这种情况下出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

1.1.1 CAS的操作过程

CAS可以通俗的理解为CAS(V,O,N)其中包含三个值分别是
V:内存地址中实际存放的值;
O:预期的值;
N:更新后的值。

  当V==O相同时,也就是说预期值和内存中实际的值相同,表明该值没有被其他线程更改过,即预期值O就是目前来说最新的值了,可以将N赋给V。
  当V!=O不相同,表明V值已经被其他线程改过了,即预期值0不是最新值了,所以不能将新值N赋给V,返回V值,将O值改为V。

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

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

1.1.2 CAS带来的问题

1.1.2.1 CAS带来的ABA问题

  线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了。
  例如一个单向链表:
在这里插入图片描述
解决方案:
  atomic包中提供了AtomicStampedReference来解决ABA问题相当于添加了版本号

1.1.2.2 自旋会浪费大量的处理器资源

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

  例如:阻塞相当于熄火停车,自旋状态相当于怠速停车。在十字路口,如果红绿灯等待的时间非常长,那么熄火相对省油一些;如果红绿灯的等待时间非常短,怠速停车更合适。

  然而,对于JVM来说,它并不能看到红灯的剩余时间,也就没法根据等待时间的长短来选择是自旋还是阻塞,JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间。即就是如果在自旋的时候获取到锁,则会增加下一次自旋的时间,否则就稍微减小下一次自旋时长,对于我们的例子就是:如果之前不熄火等待了绿灯,那么这次不熄火的时间就长一点;如果之前不熄火没等待绿灯,那么这次不熄火的时间就短一点。

1.1.2.3 CAS带来的公平性问题

  自旋状态还带来另外一个副作用,不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。然而处于自旋状态的线程,则很有可能优先获得这把锁。内建锁无法实现公平机制
Lock锁可以

2. Java对象头

  同步的时候是获取对象的monitor,即获取到对象的锁,对象的锁无非就是类似对对象的一个标志,这个标志就是存在Java对象的对象头中。Java对象头里的Mark Word里(markword是java对象数据结构中的一部分)默认存放是的对象的Hashcode,分代年龄和锁标记位。32位JVM对象头中 Mark Word默认存储结构为:

锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象的hashCode 对象分代年龄 0 01

  Java SE 1.6中,锁一共有四种状态,级别从低到高依次是:无锁状态(0 01),偏向锁状态(1 01),轻量级锁状态(00)和重量级锁状态(10),这几个状态会随着竞争情况逐渐升级,锁可以升级但是不可以降级(为了提高获得锁和释放锁的效率)

3. 偏向锁

  大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。JDK1.6之后 偏向锁默认开启
偏向锁是锁状态中最乐观的一种锁:从始至终只有一个线程请求同一把锁
偏向锁对象头的Mark Word:

偏向锁 线程ID Epoch 对象分代年龄 1 01

3.1 偏向锁的获取

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

3.2 偏向锁的撤销

偏向锁的撤销过程:
  偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
在这里插入图片描述

3.3 偏向锁的获取和撤销流程

在这里插入图片描述

4. 轻量级锁

  多个线程在不同时刻请求同一把锁,也就是不存在锁竞争的情况。针对这种状况,JVM采用了轻量级锁来避免线程的阻塞与唤醒
轻量级锁的Mark Word:

轻量级锁 指向栈中锁记录的指针 00

4.1 轻量级锁的获取

  线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获取到锁。如果失败,表示其他线程竞争锁。

4.2 轻量级锁的撤销

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

5. 三种锁的特点

偏向锁
  偏向锁只会在第一次请求锁时使用CAS操作,并在锁对象的标记字段中记录当前线程ID。在此后的运行过程中,持有偏向锁的线程无需加锁操作。针对的是锁仅会被同一线程持有的状况。
轻量级锁
  轻量级锁采用CAS操作,将对象头中的Mark Word替换为指向锁记录的指针。针对的是多个线程在不同时间段申请同一把锁的情况。
重量级锁
  重量级锁会阻塞、唤醒请求加锁的线程。针对的是多个线程同时竞争同一把锁的情况。JVM采用自适应自旋,来避免在面对非常小的同步代码块时,仍会被阻塞和唤醒的状况。

6.锁粗化

  锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次操作。将多个联系的锁扩展为一个范围更大的锁。

public class Test{
    //全局变量有线程安全问题
    public static StringBuffer sb = new StringBuffer();
    public static void main(String[] args) {
        sb.append("a");
        sb.append("b");
        sb.append("c");
    }
}

在这里插入图片描述
每次调用StringBuffer的append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一对象加锁和解锁操作,就会在在第一次append方法时进行加锁,在最后一次append方法结束后进行解锁。

7.锁消除

  删除不必要的加锁操作,如果判断一段代码中,堆上的数据不会逃逸出当前线程,则认为此代码是线程安全的,无需加锁

public class Test{
    public static void main(String[] args) {
        //sb变量是局部变量,不会有线程安全问题,加锁、解锁没有意义
        StringBuffer sb = new StringBuffer();
        sb.append("a");
        sb.append("b");
        sb.append("c");
    }
}

此时虽然append是同步方法,但是这段程序中StringBuffer属于一个局部变量,即每个线程进入此方法都会拥有一个StringBuffer的变量,互不影响,线程安全

发布了177 篇原创文章 · 获赞 466 · 访问量 14万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览