Java锁机制

概述

在并发环境下,多个线程可能会对同一资源进行争抢,那么可能会导致数据不一致的问题,为了解决这个问题,从而引入锁机制

通过抽象的锁,对资源进行锁定

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

Java对象

Java对象包括了三个部分

  • 对象头
  • 实例数据
  • 填充字节(其中对其填充字节是为了满足Java对象的大小必须是8比特的倍数这一条件设计的)

对象头

包含两部分

  • Mark Word,存储了很多和当前对象运行时状态有关的数据
  • Class Point,一个指针,指向了当前对象类型所在方法区中的类型数据

Synchronized

synchronized通过javac编译后会生成monitorentermonitorexit,依赖这两个字节码指令实现线程同步

管程

  • 首先Entry Set中聚集了一些想要进入monitor的线程,它们正处于waiting状态
  • 此时假设线程A进入到了monitor,那么他就处于Active状态,假设A线程在执行途中遇到一个判断条件,需要他暂时让出执行权,那么他将进入到Wait Set,并把状态标记为waiting
  • 此时Entry Set中的其他线程就有可能进入到Monitor中
  • 假设一个线程B进入到Monitor,并且顺利完成任务,那么它可以通过notify的形式来唤醒Wait Set中的线程,让线程A再次进入Monitor,继续执行任务

Synchronized存在性能问题

实际上是依赖于monitorentermonitorexit两条字节码指令

而Monitor是依赖于操作系统的mutex lock来实现的

Java线程实际上是对操作系统线程的映射

所以每当挂起或唤醒一个线程都要切换到操作系统的内核态,这种操作是比较重量级的

锁状态

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

锁只能升级不能降级

无锁

表示没有对资源进行锁定,所有线程都可以对其进行访问

在有竞争的情况下,通过CAS实现线程同步,CAS在操作系统中通过一条指令实现,因此它可以保证原子性

偏向锁

让对象认识某个线程,只要这个线程来访问,那么就把锁交出去,我们就可以认为这个对象偏爱这个线程

如何实现?

  • 首先先判断锁标志位是否为01,在判断倒数第三位是否为1
  • 如果是1,那么就代表当前对象的锁状态为偏向锁
  • 如果是偏向锁,就去读前23个bit,就是线程ID,通过线程ID来确认当前想要获得锁对象的这个线程
  • 假设情况发生了变化,有另外的线程来访问,那么锁就会升级为轻量级锁

轻量级锁

  • 当一个线程想要获取某个对象锁的时候,假如看到标志位为00那么就知道它是轻量级锁
  • 这时线程会在自己的虚拟机栈开辟一块Lock Record的空间
    • Lock Record存储的是对象头中Mark Word的副本以及Owner指针
  • 线程通过CAS去获取锁,一旦获得那么将会复制该对象对象头中Mark Word并且将Owner指针指向该对象
  • 并且对象Mark Word中前30bit将生成一个指针,指向该线程虚拟机栈中的Lock Record
  • 这样一来就实现了线程和对象锁的绑定
  • 其他线程想要在获取锁,就需要进行自旋(不断循环尝试获取锁)等待
    • 这种方式区别于操作系统的挂起阻塞,因为如果对象的锁很快会就被释放话,自选就不需要进行系统中断和现场恢复,效率更高
    • 但是会让CPU处于空转,于是出现了一种叫做“适应性自旋”的优化,简单来说就是自旋的时间不在固定了
      • 而是由上一次,在同一个锁上自旋的时间以及锁状态这两个条件进行决定
  • 一旦自旋等待的锁超过一个,那么轻量级锁将升级为重量级锁

重量级锁

通过Monitor对线程进行管理

乐观锁

两个重要的值:

  • Old value,代表之前读到的资源对象的状态值
  • New value,代表想要将资源更新后的值

两个操作:

  • Compare
  • Swap

这两个操作必须是原子性的

不同的架构的CPU都提供了相应的原子操作,不需要操作系统的同步原语(比如mutex),CPU已经原生地支持了CAS

Java提供的实现的CAS同步类

  • AtomicInteger

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main{
static AtomicInteger num=new AtomicInteger(0);
public static void main(String[] args){
for(int i=0;i<3;i++){
Thread t=new Thread(()->{
while(num.get()<1000){
System.out.println("thread name:"+Thread.currentThread().getName()
+":"+num.incrementAndGet());
}
});
t.start();
}
}
}

自旋的参数可以配置,默认是10次