Java锁机制的粗略总结

【多线程】月薪20K必须知道的Java锁机制的笔记

1. 什么是锁?

在并发环境下,多个线程会对同一个资源进行争抢,那么可能会导致数据不一致的问题。为了解决这个问题,很多编程语言都引入了锁机制。通过一种抽象的锁,来对资源进行锁定。

1.1 Java锁机制是怎么设计的?

在Java中,每个Object,也就是每个对象都拥有一把锁,这把锁存放在对象头中,锁中记录了当前对象被哪个线程所占用。

2. 对象、对象头、结构

对象包括了三个部分:
对象头:存放了一些对象本身的运行时信息,包含以下两部分
1. Mark Word:存储很多和当前对象运行时状态有关的数据
2. ClassPointer:指针,指向当前对象类型所在方法区中的类型数据
实例数据:初始化对象时设定的属性和状态的内容
对齐填充字节:为了满足“Java对象的大小必须是8bit的倍数”
Java锁机制的粗略总结

2.1 Mark Word

被设计得极小,只有32bit,并且是非结构化的,这样在不同的锁标志位下,不同的字段可以重用不同的比特位,因此能节省空间。
锁就存放在Mark Word中
Java锁机制的粗略总结

3. Synchronized

Synchronized被编译后会生成monitorenter和monitorexit两个字节码指令,依赖这两个字节码指令进行线程同步

3.1 Synchronized同步机制

如图
Java锁机制的粗略总结

3.2 Synchronized同步机制为什么会存在性能问题?

因为Synchronized被编译后实质上是monitorenter和monitorexit两条字节码指令,而Monitor是依赖于操作系统的mutex lock来实现的。

Java线程实际上是对操作系统线程的映射,所以每当挂起或者唤醒一个线程,都要切换操作系统的内核态,这种操作是比较重量级的,在一些情况下甚至切换时间本身将会超出线程执行任务的时间,这样的话,使用Synchronized将会对程序的性能产生很严重的影响。
Java6开始,Synchronized进行了优化,引入了偏向锁、轻量级锁

4. 无锁、偏向锁、轻量级锁、重量级锁

4.1 无锁

顾名思义,就是没有对资源进行锁定,所有线程都能够访问到同一资源,这就可能出现两种情况

  1. 某个对象不会出现在多线程环境下,或者说即使出现在了多线程环境下也不会出现竞争的情况,那么确实无须对这个对象进行任何保护,直接让他给各个线程调用就可以
  2. 资源会被竞争,但是我不想对资源进行锁定,不过还是想通过一些机制来控制多线程,比如说,如果有多个线程想要修改同一个值,我们不通过锁定资源的方式,而是通过其他方式来限制,同时也只有一个线程能够修改成功,而其他修改失败的线程将会不断尝试,直到修改成功,这就是CAS(Compare and Swap)。CAS在操作系统中通过一条指令来实现,所以他就能够保证原子性。
    Java锁机制的粗略总结

在大部分情况下,无锁编程的效率是很高的,但这并非意味着无锁能够全面代替有锁

4.2 偏向锁

假如一个对象被加锁了,但在实际运行时只有一个线程会获取这个对象锁,最理想的方式就是不通过线程状态切换,也不要通过CAS来获得锁,因为这多多少少还是会耗费一些资源

我们设想的是,最好对象能够认识这个线程,只要是这个线程过来,那么对象就直接把锁交出去,我们就可以认为这个锁偏爱这个线程,所以被称为偏向锁

通过锁标志位是01找到倒数第三位bit,如果为1则说明是偏向锁,那么再看前23位bit,判断当前线程ID是不是老顾客。
Java锁机制的粗略总结
如果发现,当前不只有一个线程,而是有多个线程正在竞争锁,那么偏向锁将会升级为轻量级锁

4.3 轻量级锁

不像偏向锁一样按照线程ID来找到占有这个锁的线程,而是查看前30位bit的指向栈中锁记录的指针。

当一个线程想要获得某个对象的锁时,假如看到锁标志位位00,那么就知道它是轻量级锁。
此时,线程会在自己私有的虚拟机栈中开辟一块被称为Lock Record的空间。
LockRecord中存放的是对象头中的Mark Word的副本,以及owner指针。

4.3.1 轻量级锁和线程的绑定过程

线程通过CAS去尝试获取锁,一旦获得那么将会复制该对象头中的Mark Word到Lock Record中,并且将Lock Record中的owner指针指向该对象。

另一方面,对象的Mark Word的前30个bit将会生成一个指针,指向线程虚拟机栈中的Lock Record。
Java锁机制的粗略总结

4.3.2 轻量级锁锁定后,其他线程想获取该怎么办?

其他线程将会自旋等待

自旋
可以理解为一种轮询,线程自己在不断的循环,尝试着去看一下目标对象的锁有没有被释放,如果释放了就获取,如果没有就进行下一次循环。

这种方式区别于被操作系统挂起,因为如果对象的锁很快就会被释放的话,自旋就不需要进行系统中断和现场恢复,所以效率更高。

自旋相当于是CPU在空转,如果长时间自旋将会浪费CPU资源,于是出现了一种叫做“适应性自旋”的优化。

适应性自旋
简单来说,就是自旋的时间不再固定了,而是由上一次在同一个锁上的自旋时间以及锁状态,这两个条件来进行决定。
举个例子,比如在这个锁上,当前正在自旋等待的线程刚刚已经成功获得过锁,但是锁目前是被其他线程占用,那么虚拟机就会认为这次自旋也很有可能会成功,进而它将允许更长的自旋时间。

如果自旋等待的线程数超过CPU核数的一半,或者自旋次数超过10次就会升级称重量级锁

4.4 重量级锁

如果被标记为重量级锁,那么就和最初的那样,需要通过Monitor来对线程进行控制,此时将会完全锁定资源,对线程的管控最为严格