本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
synchronized 锁
synchronized是原子性内置锁,又称监视器锁。编译器会在同步代码块前后加上monitorenter和monitorexit字节码,其依赖操作系统底层互斥锁实现。该关键字主要作用是实现原子性操作和解决共享变量的内存可见性问题。
在执行monitorenter指令时会尝试获取对象锁,如果该对象没有被锁定 / 已经获得了锁,则锁计数器+1,其它竞争的线程进入等待队列中。执行monitorexit执行时将锁计数器-1,当计数器值为0时,释放锁,处于等待队列中的线程再次竞争锁。
synchronized时排他锁,当一个线程获得锁之后其他线程必须等待该线程释放锁才能获得锁,且由于Java中线程和操作系统原生线程是一一对应的,线程被阻塞 / 唤醒时会从用户态切换到内核态,这种转换非常消耗性能。
从内存予以上讲,枷锁的过程会清除工作内存中的共享变量,再从主内存读取,释放锁的过程则是将工作内存中的共享变量写回主内存。
详细机制
- 当多个线程进入同步代码块时,首先进入entryList
- 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
- 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
- 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null
锁的优化机制
在JDK1.6以后,synchronized本身不断优化锁的机制,优化为包括 自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。锁的状态从低到高依次为 无锁->偏向锁->轻量级锁->重量级锁,锁一般情况下会升级,一定条件下有可能发生降级。
- 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用
-XX:PreBlockSpin设置。 - 自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
- 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
- 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
- 偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS
(Compare and Swap)来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。 - 轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。