Java中的锁

365 阅读4分钟

一 锁相关的概念

1.1 锁的内存含义

  • 原子性
  • 可见性
当一个线程执行完成后,它对内存的修改必须立刻对其他线程可见
  • 有序性
禁止指令的重排序

1.2 锁的种类

自旋锁,悲观锁,乐观锁,读写锁,共享锁,可重入锁/不可重入锁,公平/非公平锁

二 Java内存模型

2.1 计算机体系结构

内存
程序都是运行在内存的,内存会保存程序运行的数据,供cpu使用
cpu
cpu首先去L1 Cache-->L2 Cache--->L3Cache中找数据,如果没有找到,再去内存中寻找

2.2 JMM(Java内存模型)

作用
用来控制多线程访问数据的一套协议(保证可见性,原子性以及有序性)
JMM模型

没一个线程都有一个工作内存,线程所有的操作都是在工作内存中进行
工作内存和主内存的交互关系是通过如图下面的8个原子操作完成的

1.read 
2.load
3.use
4.assign
5.wtite
6.store
7.lock
8.unlock

cpu结构和JMM关系

三 synchronized关键字

3.1 synchronized的特性

保证了原子性,可见性,有序性,可重入,不可中断

3.2 重量级锁monitor

  • owner
记录拥有当前锁的对应的线程,通过CAS操作实现
  • cxq
一个单向链表,所有的竞争锁的线程都首先放在这个队列,进行自旋获取锁,自旋一段时间失败后,进行挂起

  • entryList
EntryList是一个双向链表。当EntryList为空,cxq不为空,Owener会在unlock时,
将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程为OnDeck线程。
  • WaiterList
WatiList是Owner线程地调用wait()方法后进入的线程。
进入WaitList中的线程在notify()/notifyAll()调用后会被加入到EntryList。
  • OnDeckThread
可进行锁竞争的线程。若一个线程被设置为OnDeck,则表明其可以进行tryLock操作,若获取锁成功,则变为Owner,否则仍将其回插到EntryList头部。

3.2.1 重量级锁的获取流程

1.全部竞争线程都进入到cqx中,尝试获取锁,没有获取到锁则阻塞
2.操作系统从entryList的队尾中选择一个线程,设置为OnDeckThread,进行锁的竞争,竞争成功后获取锁,失败后依旧放在entryList的队尾
3.当释放锁的时候,设置一个OnDeckThread进行锁竞争

3.3 锁升级过程

偏向锁-->轻量级锁--->重量级锁

3.3.1 对象头

Java一个对象包含对象头,实例数据,对齐数据
对象头如下

锁的整个升级的流程是靠对象头实现的

3.3.2 锁升级

偏向锁

优点

为了消除无竞争状态下的进行的同步原语,进一步提升性能

偏向锁的获取

当一个线程访问同步块并且获取获取锁的时候,会在对象头和栈帧中的锁记录存储偏向的线程ID,以后这个线程进入和退出同步块的时候不需要进行加锁和解锁,
再次进入到同步块的时候,简单测试下对象头中的MarkDown是否存储了指向当前线程的偏向锁,如果测试成功,就表示线程已经获取到了锁,
如果测试失败,则需要判断一下是否在MarkDowm中的开启了偏向锁,
如果开启偏向锁,则尝试将对象头的偏向锁指向当前线程

偏向锁的撤销
偏向锁不会撤销,只会升级到轻量级锁

请求偏向锁失败后,在一个安全点进行stop-the-word,判断markdown中指向的线程是否存活,如果存活的话,让其获取到轻量级别锁,否则直接升级当前锁为轻量级锁.

偏向锁的用处

假设同步代码块永远只有一个线程执行,在这种情况下只需要第一次进行CAS操作,其余连CAS操作都不需要

轻量级锁

在偏向锁获取失败的时候,锁升级为轻量级锁
轻量级锁的加锁

轻量级锁的解锁

轻量级锁的使用场景

1.两个线程A和线程B轮流访问一个同步代码块,这个时候就是使用轻量级锁  
2.当线程A和线程B同时访问一个同步代码块时候,轻量级锁对应的线程会首先自旋转,自旋没有获取到锁的时候,会将锁升级为重量级锁

3.4 总结

1.从开始到结束,只有一个线程访问代码块,使用的是偏向锁
2.两个线程轮流访问,使用的是轻量级锁
3.两个线程并发访问,使用的是重量级锁

3.5 synchronized关键字对应多

1.加在同步方法上,使用的是对应的对象
2.加咋静态方法上,使用的是类遍历
3.对于同步方法块,锁是Synchronized中括号对应的对象