Java锁机制
Java锁机制
概述
在并发环境下,多个线程可能会对同一资源进行争抢,那么可能会导致数据不一致的问题,为了解决这个问题,从而引入锁机制
通过抽象的锁,对资源进行锁定
在Java中,每个object,也就是每个对象都拥有一把锁,这把锁记录在对象头中,锁中记录了对象被哪个线程所占用
Java对象
Java对象包括了三个部分
- 对象头
- 实例数据
- 填充字节(其中对其填充字节是为了满足Java对象的大小必须是8比特的倍数这一条件设计的)
对象头
包含两部分
- Mark Word,存储了很多和当前对象运行时状态有关的数据
- Class Point,一个指针,指向了当前对象类型所在方法区中的类型数据
Synchronized
synchronized
通过javac编译后会生成monitorenter
和monitorexit
,依赖这两个字节码指令实现线程同步
管程
- 首先Entry Set中聚集了一些想要进入monitor的线程,它们正处于waiting状态
- 此时假设线程A进入到了monitor,那么他就处于Active状态,假设A线程在执行途中遇到一个判断条件,需要他暂时让出执行权,那么他将进入到Wait Set,并把状态标记为waiting
- 此时Entry Set中的其他线程就有可能进入到Monitor中
- 假设一个线程B进入到Monitor,并且顺利完成任务,那么它可以通过notify的形式来唤醒Wait Set中的线程,让线程A再次进入Monitor,继续执行任务
Synchronized存在性能问题
实际上是依赖于monitorenter
和monitorexit
两条字节码指令
而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 | public class Main{ |
自旋的参数可以配置,默认是10次
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 WereAsh!
评论
ValineDisqus