理解synchronized原理之前,先看一个例子:
public class Application implements Runnable{
}
两个线程先后被执行,一个线程必须在另一个线程执行完后才能执行,因为两个线程使用同一个当前实例对象锁,只有一个线程释放该锁后,另一个线程才能拿到该锁。Synchronized的原理
分析其原理其实就是分析加锁和释放锁,通过反编译得到两个最重要的指令Monitorenter和Monitorexit,每一个锁对象同一时间只与一个monitor相关联,每一个锁对象在同一个时间只能被一个线程获得。一个对象锁获得monitor所有权后,monitor中计数器会发生变化。 一个线程在获得对象锁之前,monitor计数器必须为0,否则需要等待其他线程释放锁。一旦线程获得锁后monitor计数器加一,其他线程处于等待状态。如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加。 一个线程释放锁就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。 下图展示了线程、对象锁、同步队列、加锁、释放锁之间流程关系:可重入原理:一个线程执行两个静态方法,但是两个方法拥有同一个对像锁,当一个方法被执行完后不需要再次获得该锁,直接计数器加一继续执行,只有一条monitorexit指令,并没有monitorenter获取锁的指令。
synchronized的锁类型
JVM中锁的monitorenter和monitorexit依赖底层操作系统中Mutex Lock来实现,我们知道传统锁操作需要很大系统性能开销。在单线程无抢锁环境下将严重浪费不必要系统性能开销。基于此jvm中对锁进行了优化,出现了轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自旋锁与自适应自旋锁等技术减少锁操作带来的性能开销。synchronized同步锁一共有四种状态按照锁竞争升级依次为无锁、偏向锁、轻量级所、重量级锁。根据情况不同,锁会出现处于不同状态,目的是为了获取锁和释放锁提高效率,节省系统性能开销。
重量级锁:
传统锁jvm中内置锁在有多个线程竞争情况下,一个线程抢到该锁,其他线程被挂起和阻塞直到锁被释放,监视器锁直接对应底层操作系统中的互斥量(mutex lock)。系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等,这种同步方式的成本非常高,这种锁为“重量级锁”。
自旋锁:
在传统没有锁优化加入进来情况下,多线程在竞争一个锁时,出现如下情况:
在很大一部分情况下,一个线程对一个锁拥有时间很短,其他线程为了这一段很小时间去调用操作系统内核态实现挂起和恢复操作,这对操作系统带来很大且不必要的性能压力。在此情况下出现自旋锁,让另一个没有获取到锁的线程在门外等待一会(自旋),等待持有锁的线程释放锁。
优点:拥有锁的线程占用锁时间非常短,自旋锁性能就会非常好。
缺点:线程在自旋会一直占据cpu资源,如果锁占用的时间太长,那么自旋的线程会白白消耗掉CPU资源。因此必须设定自旋次数。默认为10次,超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了。但是有一个问题线程锁在线程自旋刚结束就释放掉了锁,那么是不是有点得不偿失。所以这时候我们需要更加聪明的锁来实现更加灵活的自旋。
自适应自旋锁:
意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到100此循环。相反,如果对于某个锁,自旋很少成功获取锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。