Java的每一个对象都可以被Synchronized关键字上锁,包括:实例对象、指定对象、类的class对象。(实例方法、静态方法、代码块)
Synchronized的底层原理:
任意线程对Object的访问(该Object由Synchronized关键字保护时),首先会获得Object的监视器Monitor,这个获取过程是排他的,也就是同一时刻只能有一个现成获取到synchronized所保护对象的监视器。如果获取失败,则线程进入同步队列,状态变为BLOCKED。当获得了锁的线程执行完毕,释放monitor后,会释放操作会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。
可重入:Synchronized加锁的对象都拥有一个Monitor计数器,当线程获取该对象锁后,monitor计数器就会加一,释放锁减一。当同一个线程反复对该对象加缩时,只会引起monitor计数器加一,只有当最后一次释放锁,monitor计数器值为0时才被表示真正释放锁。
可见性:Synchronized的happen-before规则,即监视器锁规则:对同一个监视器的解锁,happen-before于对该监视器的加锁。根据happen-before的定义:如果Ahappen-before于B,那么A的执行结果对B可见,并且A的执行顺序先于B。
锁的内存语义:
- 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中;
- 当线程获取锁时,JMM会把线程对应的本地内存置为无效。
锁升级:
Synchronized锁一共有4个状态,分别是:无锁、偏向锁、轻量级锁、重量级锁,这几个锁状态会随着并发竞争情况逐渐升级,并且锁只能升级不能降级。
- 偏向锁:
在大多实际情况下,锁不仅不存在多线程竞争,反而总是由一个线程对获取,那么在同一个线程反复获得锁释放锁中,没有锁的竞争,因此,多次获得锁和释放锁的操作带来了很多不必要的性能开销和上下文切换。因此引出偏向锁的概念。
偏向锁的加锁:当一个线程访问同步块并获取锁时,会在对象头存储锁偏向该线程ID,之后该线程在进入和退出同步块时就不需要进行CAS操作来加锁和解锁,只需测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果成功,那么该线程获得锁;失败则需要再测试一下当前锁的标识是否为1(表示当前是偏向锁),如果是1,那么尝试用CAS将对象头的偏向锁指向当前线程。如果不是1则使用CAS竞争锁。
偏向锁撤销:指的是偏向锁升级为轻量级锁之前,需要将对象头的线程ID删除。
- 轻量级锁:
当偏向锁被撤销后,锁升级成了轻量级锁,轻量级锁的各个线程需要通过自旋+CAS的方式争夺锁。两个线程竞争的过程就是通过CAS的方式将Mark Word中存储的信息转为指向自己LR(Lock,Record,锁记录)的指针。其他线程就一直自旋获取锁(自旋:占用CPU资源反复地获取锁,直至获取成功,或者自旋一定次数)。当一个线程自旋一定次数(一定时间)仍未获取锁,那么该线程把锁修改为重量级锁,并让所有竞争该锁的线程进入阻塞状态,等待获取锁线程执行完后唤醒。
重量级锁:
线程加锁失败会进入阻塞队列,等待前去获得线程的锁执行完之后唤醒。线程的阻塞和唤醒都设计操作系统内核态和用户态切换,因此性能的开销较大。