特性
原子性
原子性就是一个操作或者多个操作要么一起执行,要么不执行。比如i++就是不具备原子性,它是先取出i的数据再进行加1,那这个过程在多线程中可能会出现脏读的情况。(Volatile与Sychronized的最大区别就是不具备原子性)
可见性
Sychronzied被加锁对象的锁对于每个线程都是可见的,都能获取到锁的状态,在释放锁之前,会把最新的值更新到主存,保证值都是最新状态。
Volatile修饰的变量,每当值要改变时,都会立即更新到主存中,主存的内容对于其他线程都是可见的,就能保证值都是最新状态的。
有序性
Sychronized和Volatile都具有有序性,Java编译器允许指令重排,但指令重排不会影响单线程执行顺序,会影响多线程的并发执行顺序,加了锁后就保证每次都只有一个线程访问,保证有序性。
可重入性(可重入锁)
Sychronized、ReentrantLock和Volatile都是可重入锁,当一个线程试图操作一个其他线程持有的对象锁的临界资源时(临界资源就是只能有一个线程使用的资源),那这个线程会处于阻塞状态。可重入就是同一个线程可以重复获取对象锁。可重入锁的作用是为了防止死锁,比如一个加锁的方法a调用另一个加锁的方法b,它们是用一个线程的执行代码,会在a那里拿到锁往下执行,然后b也要去获取对象锁才能执行,如果不能重入,则会在b这里形成死循环。
底层实现
对象在JVM中的存储形式
在了解Sychronized原理前,需要了解下对象在JVM中的存储,一共三个部分,为对象头、实例数据、对齐填充。
实例数据和对齐填充与Sychronized无关,实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;对齐填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。
对象头就是实现Sychronized的重点,申请锁、上锁、释放锁都是在对象头上进行的,对象头主要结构是由Mark Word
和 Class Metadata Address
组成,其中Mark Word
存储对象的hashCode、锁类型状态等信息或分代年龄或GC标志等信息,Class Metadata Address
是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。在申请锁、升级锁时都要读取对象的Mark Word。
具体实现
每个锁都会对应一个monitor对象,会随对象一起创建销毁或当线程试图获取对象锁时会生成一个monitor对象。什么是monitor对象?monitor对象是由C++实现的,它可以获取ObjectMonitor指针,对象头存的是ObjectMonitor的指针,它是由hotspot虚拟机中的ObjectMonitor对象实现的,它的结构信息看下图
当一个线程获取锁时,monitor中的count自增1,owner设为当前线程,整个锁处于锁定状态,当执行wait或者执行完代码块的持有线程,将会复位相关的状态,比如count-1,owner为null,这时候锁为空闲状态,等待中的线程就可以得到。
锁类型
在JDK1.6之前,只有两种锁状态:无锁和重量级锁。在JDK1.6之后,增加了两种状态:偏向锁和轻量级锁。通过锁的优化:锁消除、锁粗化、自旋锁等各种使用场景,给Sychronzied带来了很大的提升。
锁膨胀
什么是锁膨胀,或者说是锁升级,为了提高获得锁和释放锁的效率,会根据实际情况进行膨胀升级,它的膨胀方向是:无锁——>偏向锁——>轻量锁——>重量锁,这个过程不可逆。
偏向锁
偏向锁就是当只有一个线程去执行同步代码块时,就是偏向锁,不存在多线程竞争,再次请求锁时,不需要申请,只是对比Mark Word中的锁标记是不是偏向锁和线程id是不是一致就可以,这样省去了大量锁申请的操作。
意义:锁偏向于第一个获得它的线程。如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
轻量锁
当有第二个线程去申请锁时,就会升级为轻量锁。其他线程会通过自旋的方式去申请锁,自旋锁的实现方式是CAS,通过循环不断的申请,但会耗费CPU时间片(循环过程中线程会一直处于running状态,但是基于JVM的线程调度,升级为阻塞锁,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会,下面会说自旋锁的优化),阻塞锁会放弃CPU时间片但在上下文的切换会存在消耗,所以在临界区小的时候采用自旋的方式是效率比较高的,当临界区大的时候,自旋就会随着时间的推移产生很多性能问题。
当自旋获取锁失败时,说明有其他的线程也在竞争锁,则会升级为重量锁。
重量锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
锁的总结与对比
锁优化
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。
锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。
自旋锁的优化
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
**自旋锁:**许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起(也就是阻塞锁),这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。