Java并发机制的底层实现
本文最后更新于:2024年4月18日 晚上
volatile
在多线程并发编程中synchronized和volatile都扮演着重要的角色,相较于synchronized,volatile更加轻量级,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能同时读到这个修改的值。
volatile变量修饰的共享变量进行写操作的时候会使用处理器提供的Lock指令,将这个变量所在缓存行的数据写回到系统内存。但在多核处理器下,写回内存不能保证其他核缓存一致。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,会重新从系统内存中把数据读到处理器缓存里。Lock信号还有一种机制是利用总线锁,在总线上声言LOCK#信号。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。
volatile的优化
CPU在读内存时一次读一个块(cache line 缓存行),64字节,因为读取一个数据后往往会读它相邻的数据。
在X86上,CPU一般的告诉缓存是64个字节宽,不能填充部分缓存行,如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。
在Java 7中,它会自动淘汰或重新排列无用字段,需要使用其他追加字节的方式。
synchronized
相较于volatile关键字,synchronized会更加重量级一点,它是Java中最基本的同步手段,它保证了线程的安全性,但是它的性能开销也是比较大的。
synchronized主要有三种用法:
- 修饰实例方法
- 修饰静态方法
- 修饰同步方法块中的对象
synchronized的实现是基于对象头中的Mark Word和锁记录来实现的,当一个线程获取锁的时候,会在锁记录中记录锁的持有者,当锁被释放的时候,会清空锁记录中的持有者信息。synchronized代码块的同步是通过monitorenter和monitorexit指令来实现的,而方法同步是另一种发放实现的。
锁升级的过程
对于同一问题的处理,并不一定是创建的线程数量越多,执行越快,这是由于线程有创建和上下文切换的开销。而锁的上下文消耗尤为严重,因此JDK6对锁进行了优化。
JDK 1.6引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。这几个状态会随着竞争逐渐升级,锁可以升级但不能降级。
锁升级的过程
- 无锁状态:锁对象还没有被任何线程锁定,没有线程相互竞争,此时处于无锁状态。
- 偏向锁状态:只有一个线程不停访问代码块,此时会使用偏向锁。(在JDK15后,偏向锁被废弃,因为撤销带来的性能开销大)
- 轻量级锁状态:多个线程访问代码块,偏向锁会撤销,此时会使用轻量级锁。
- 重量级锁状态:当CAS自旋达到一定次数没有拿到锁(有线程超过10次自旋,或者自旋线程数超过cpu核数的一半),会撤销掉轻量级锁,此时会使用重量级锁。
graph LR A[无锁状态] -->|有一个线程竞争锁| B[偏向锁状态] B -->|多个线程竞争| C[偏向锁状态] C -->|CAS自旋达到一定次数| D[重量级锁状态]
相较于volatile的区别
- volatile主要用于修饰变量,而syschronized主要用于修饰方法和代码块;
- volatile相较于synchronized速度更快;
- volatile关键字是Java中的一种轻量级同步机制,它保证了线程的可见性,但是不保证原子性;
- synchronized关键字是Java中的一种重量级同步机制,它保证了线程的可见性和原子性,但是性能开销较大;
- volatile关键字一般使用乐观锁的思想实现,通常使用轻量级锁,使用CAS机制。
- synchronized关键字一般使用悲观锁的思想实现,通常有锁升级的过程。
CAS
CAS操作是一种乐观锁的实现方式,它的实现是基于CPU的原子操作指令,当一个线程获取锁的时候,会尝试使用CAS操作来获取锁,如果CAS操作失败,那么就会尝试重新获取锁,直到获取锁成功。
CAS中文名为比较并交换,它是一种无锁的实现方式,它的实现是基于CPU的原子操作指令,当一个线程获取锁的时候,会检查数值
问题
ABA问题
ABA问题是指在CAS操作中,如果一个线程在获取锁的时候,另一个线程将锁的值从A改为B,然后再改回A,在使用CAS进行检查时,会发现数值没有变化,但实际上发生了变化,这就是ABA问题。
解决ABA问题的方式是使用版本号,每次修改数值的时候,都会修改版本号,这样在使用CAS进行检查的时候,就可以检查版本号是否发生变化。
循环时间长开销大
CAS操作是一种乐观锁的实现方式,它的实现是基于CPU的原子操作指令,当一个线程获取锁的时候,会尝试使用CAS操作来获取锁,如果CAS操作失败,那么就会尝试重新获取锁,直到获取锁成功。但是在多线程竞争激烈的情况下,会导致CAS操作的循环时间长,开销大。
只能保证一个共享变量的原子操作
CAS操作只能保证一个共享变量的原子操作,对多个变量进行操作时,CAS无法保证原子性。如果要保证多个共享变量的原子操作,就需要使用synchronized关键字。
各种锁的对比
偏向锁
偏向锁是一种针对加锁操作的优化手段,它的目标是减少无竞争的情况下,减少不必要的轻量级锁和重量级锁的开销。偏向锁的实现是基于CAS操作的,当一个线程获取锁的时候,会在对象头中的Mark Word中记录锁的持有者,当另一个线程获取锁的时候,会检查Mark Word中的持有者信息,如果是自己,那么就可以直接获取锁,如果不是自己,那么就会尝试使用CAS操作来获取锁。
轻量级锁
轻量级锁是一种针对竞争不激烈的情况下的优化手段,它的目标是减少重量级锁的开销。轻量级锁又称为自旋锁、无锁、自适应自旋,运行在用户态。轻量级锁的实现是基于CAS操作的,当一个线程获取锁的时候,会在对象头中的Mark Word中记录锁的持有者,当另一个线程获取锁的时候,会检查Mark Word中的持有者信息,如果是自己,那么就可以直接获取锁,如果不是自己,那么就会尝试使用CAS操作来获取锁,如果CAS操作失败,那么就会升级为重量级锁。
重量级锁
重量级锁是一种针对竞争激烈的情况下的优化手段,它的目标是保证线程的安全性。重量级锁的实现是基于操作系统的互斥量来实现的,当一个线程获取锁的时候,会将锁的状态设置为锁定状态,当另一个线程获取锁的时候,会检查锁的状态,如果是锁定状态,那么就会进入等待队列,等待锁的释放。