Java中多线程的技术必然要了解的必然是锁~锁保证多线程的数据安全。所以不多说进入主题。
最轻量级的锁Volatile
当然这是因为它只能修饰变量,而且相对于synchronized关键字轻量了很多~
对于变量增加了volatile的修饰,表示该数据的写和读都直接同步到主内存,所以保证该变量的任何修改对于其他线程是可见的。
我们可以查看关于修饰volatile变量的编译文件和指令,可以看出~里面会有一个lock的汇编指令。具体实现是通过内存屏障,具体如图:
同时volatile还有一个功能就是防止指令重排序。java代码到最终执行的机器码要经过几次指令排序~如下图:
但是volatile有些时候可能会导致误用,比如
volatile int i=0;
{
i++;
}
这段代码再多线程的时候可能会出现问题~具体原因是i++非原子操作。可能存在覆盖的情况
synchronized
这个关键字应该很不陌生。Java 1.6之后~由于synchronized的锁太重性能很是问题,所以升级为偏向锁,轻量锁,重量锁,根据不同情况使用不同锁。
作用范围
- 修饰实例方法
表示对于当前实例加锁,进入任何该对象实例修饰的代码块均需要提前获得锁。 - 修饰静态方法
表示对于该类进行加锁,进入该类修饰的代码块需要获取当前类的锁。 - 修饰代码块
指定加锁对象,进入加锁对象的代码块~需要提前获得该对象锁
锁的存储
由于所是锁资源对象~所以要了解一下对象在堆上的存储。一共分为三个区域对象头(header),示例数据(instanceData),和对齐填充(padding)。如图:
通过cpp源码可以了解到~一个对象的创建都会创建一个instanceOopDesc(数组是arrayOopDesc)在文件instanceOop.hpp里面。
可以看到它集成了oopDesc,而oopDesc里面包含两个成员变量(_mark,_metadata).其中_mark是markOop类型,也就是所谓的Mark World。记录了对象和锁相关的数据。
MarkWorld
markOop类型记录在markOop.hpp文件中,如下:
该部分记录了和锁相关的所有信息~MarkWorld具体会有5种情况:
锁的类型
1.6后锁的类型分为无锁,偏向锁,轻量锁,重量锁。
偏向锁
最简单的锁,支持重入,场景:经常是单线程进行锁定~即在对象_mark中通过CAS标记为偏向锁,同时将线程ID记录在对象中即可。流程如下:
可以通过JVM参数 UseBiasedLocking来控制是否开启。
当通过CAS失败获取偏向锁的时候,就会升级为轻量锁
轻量级锁
当偏向锁存在竞争的时候,即CAS设置偏向锁的时候失败,那么失败的线程将进行自旋操作(自旋锁)。自旋锁即采用for循环,不断的去进行获取锁的操作,所以针对同步代码块执行快的操作,是ok的。但是如果过长,则会造成cpu资源的浪费。
轻量级锁的更新流程:
而锁对象的markWorld也进行相应的更改
- 线程在自己的栈里面创建LockRecord
- 将锁对象的markWorld复制过来
- 将LockRecord的owner指针指向锁对象
- 将锁对象的MarkWorld替换为指向LockRecord的指针
如下图:
随后更改后:
可以通过JVM参数preBlockSpin来控制自旋数量
当CAS设置锁对象的MarkWorld失败的时候,就会膨胀微重量级锁。
重量级锁
重量级锁就是1.5之前的synchronized锁~通过java -p看class信息可以看出指令即monitorenter和monitorexit来控制锁的加锁和释放。
所有的Java Object天生携带一个monitor,可以认为是一个同步对象,通过对于对象监视器的争抢修改锁标志从而进行多线程同步。在markOop.hpp源码可以看到:
monitor的操作依赖于操作系统的MutexLock(互斥锁)来实现的,线程一旦被阻塞,那么就会从用户态切换到内核调度状态。频繁的内核态和用户态的切换回影响锁的性能。锁竞争monitor的逻辑如下图:
最后贴一张基于wait和notify的锁加入和释放的图:
抛砖引玉,整理学习笔记。如有问题随时沟通交流~谢谢