synchronized与锁

143 阅读4分钟

这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

java中的锁都是对象,对象头中的mark word中保存着锁信息。锁的作用是控制线程竞争进入同步块从而实现线程间的同步。

java中的锁分为四个级别,分别为无锁、偏向锁、轻量级锁、重量级锁。

  1. 无锁:即没有锁来控制同步。
  2. 偏向锁:顾名思义,偏向锁偏向于第一个获取该锁的线程。当锁被第一个线程得到以后,该线程会在对象头中的mark word和栈帧中记录自己的线程ID,下次线程再次尝试进入同步块,会去mark word中检查记录的是否是自己的线程ID,若是,则表示该线程已经获取了该偏向锁,进入退出同步块时无需再做加锁解锁操作(synchronized中的加锁解锁是用cas语句实现的)。若不是,则表示有其他线程竞争锁,该线程尝试使用cas语句将mark word中的线程ID替换为自己的线程ID,如果替换成功,则表示之前的线程不存在了,该锁依然是偏向锁,新线程获取该锁;如果替换失败,则表明之前的锁还存在,挂起持有该偏向锁的线程,修改mark word中的标志位,升级为轻量级锁,线程按照轻量级锁的方式竞争锁。线程获取偏向锁后会一直持有该锁,直到发生竞争才会释放偏向锁(允许其他线程将自己的线程ID写入markWord)
  3. 轻量级锁:JVM会为每个线程在其栈帧中创建存储锁记录(lock record)的空间(该空间称为Displaced Mark Word),线程在尝试获取锁的时候,发现该锁为轻量级锁(mark word中标志位00),则会将Mark Word信息复制到自己的Displaced Mark Word中并尝试用CAS将锁的Mark Word替换为指向自己栈中锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋(不断尝试去获取锁,一般用循环来实现)来获取锁。JDK采用自适应自旋,若线程自旋成功获得锁了则下次自旋次数会更多,若自旋失败则下次自旋次数会减少(因为自旋失败很可能之后自旋也失败,于是减少自旋次数,毕竟自旋也是要耗费资源的)。若自旋未成功当自旋次数超过一定次数(和JVM、操作系统相关)仍然没获取锁则该线程阻塞,轻量级锁升级为重量级锁。在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。
  4. 重量级锁:当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到竞争队列的队列,然后调用park函数挂起当前线程。当线程释放锁时,会从竞争队列中挑选一个线程唤醒,被选中的线程被唤醒后会尝试获得锁,但synchronized是非公平的,所以该线程不一定能获得锁。这是因为对于重量级锁,线程先自旋尝试获得锁,这样做的目的是为了减少执行操作系统同步操作带来的开销。如果自旋不成功再进入等待队列。如果线程获得锁后调用Object.wait方法,则会将线程加入到WaitSet中,当被Object.notify唤醒后,会将线程从WaitSet移动到Contention List或。需要注意的是,当调用一个锁对象的wait或notify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。