synchronized作用
让一段代码在多线程之间互斥访问,保证原子性、有序性、可见性,并且是可重入的。
synchronized使用方式
- synchronized修饰代码块时需要传入一个对象作为锁,这个对象可以是任意的,进入同步代码块前要获取这个锁,没有获得锁的线程就要阻塞等待,同步代码块执行完释放锁。
- 修饰成员方法时,锁是对象实例。
- 修饰静态方法时,锁是类的class实例。
synchronized原理综述
每个Java对象都关联一个监视器锁对象,同步代码块在字节码层面是通过在代码块的指令前后加上monitorenter和monitorexit指令来标识的。
当线程执行到monitorenter指令时,当前线程试图获取监视器对象所有权,如果未加锁或者已被当前线程持有,就把锁计数加1;执行到monitorexit指令的时候,锁计数减1。锁计数为0时,锁就被释放了。
如果当前线程获取监视器对象失败,线程会阻塞等待,线程会作为一个结点存入监视器对象的锁池队列中,等待唤醒。
监视器对象还有一个等待池队列,在同步代码块中调用锁对象的wait方法的时候,线程会阻塞,并被封装为一个结点,进入监视器对象的等待池队列,调用锁对象的notify和notifyAll的时候,会把等待池队列中的线程放入锁池队列中,只有锁池中的线程才能参与竞争锁。
wait和notify用来线程之间同步过程的,比如实现生产者消费者模型。
监视器锁底层是通过操作系统的互斥锁实现的,操作系统做线程切换需要从用户态转换到内核态,所以比较消耗性能。这种依赖于操作系统互斥锁而实现的锁叫做重量级锁。
如果线程竞争并不激烈,切换线程的开销是不划算的,特别是多个线程在不同的时段获取同一把锁,可以用自旋锁,就是CAS+循环,CAS由硬件之间实现,开销比切换线程开销要小,这就是轻量级锁,在JDK6引入。
锁信息具体存储在对象头的MarkWord里,一个对象在虚拟机的内存布局分为三部分,对象头、实例数据、对齐填充,其中对象头又分为MarkWord、类型指针、数组长度,MarkWord存储了锁相关的信息、hashCode、垃圾回收的分代年龄等。
轻量级锁的CAS操作具体修改的是锁对象头的mark word,修改为指向当前线程栈帧中的锁记录的指针。虚拟机会为每个线程在当前线程栈帧中创建一块锁记录空间,存储锁对象的对象头中的mark word的拷贝。
如果CAS修改成功,当前线程就获得了轻量级锁。
如果更新失败,说明有线程竞争,开始自旋,自旋超过一定次数,升级为重量级锁。避免竞争激烈的情况下自旋空耗CPU。
如果压根都没有线程竞争,只有单线程访问同步代码块,其实每次进行CAS操作获取轻量级锁也是没有必要的开销,所以JDK6又引入了偏向锁,只在第一次申请锁的时候,对Java对象头的MARK WORD做CAS操作,记录下当前申请锁的线程ID,退出同步代码块后,也不释放锁,锁对象的对象头中记录的还是当前的线程ID,同一个线程再次进入这个锁的同步代码块的时候,检查锁的对象头中记录的线程ID是否还是自己,是的话就直接进入,不用再CAS了。当有多个线程要申请锁的时候,就会升级到轻量级锁。
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
ReentrantLock与synchronized的区别?
ReentrantLock与synchronized有相同的功能和语义,添加了更多灵活的功能。
synchronized能干的ReentrantLock都能干,ReentrantLock能干synchronized不一定能干。
最主要区别有三点:支持等待可中断、可实现公平锁、锁可以绑定多个条件。
详细区别:
| |ReentrantLock|synchronized
|—|—|—|
|底层实现|继承AQS|监视器模式
|灵活性|支持响应中断、超时、尝试获取锁|不灵活
|释放锁形式|必须显式调用unlock()释放锁,可跨方法调用|自动释放监视器,不可跨方法释放
|锁类型|公平锁和非公平锁|非公平锁
|条件队列|可关联多个条件队列|只有一个条件队列
|错误排查|没有释放锁,很追溯发生错误的位置,因为没有记录应该释放锁的时间和位置难度|synchronized加锁解锁过程有完整的日志
该用ReentrantLock还是synchronized?
除非需要ReentrantLock的特定的功能,否则还是应该优先使用synchronized,因为简单易用,不会忘记释放锁。
ReentrantLock实现原理
底层通过继承AbstractQueuedSynchronizer实现。
大致过程:
- 先通过cas修改int类型的state变量,修改成功则获取互斥锁成功。
- cas修改失败则把当前线程生成一个节点,放入一个等待队列中,线程被挂起。
- 等待队列操作也都是通过cas加自旋的方式来完成,避免同步开销。
- 释放锁后,会唤醒队列去自旋CAS去修改state来获取互斥锁。
state变量记录锁的重入次数。
synchronized锁升级
- jdk1.6之前synchronized直接就上重量级锁。
- jdk1.6开始synchronized会先由无锁转为偏向锁,再转为轻量级锁,再转为重量级锁,锁只能升级不能降级。
偏向锁、轻量级锁是针对synchronized优化。
对象头
JVM中,对象在内存中除了存储对象本身的数据,还会额外存储关于对象的一些附加信息。
- 普通对象的对象头中存储mark word和类型指针(指向对象所属类的指针)。
- 数组对象还会存储数组长度。
mark word存储对象的hashcode、GC分代年龄、锁状态等信息。
mark word长度在32位系统上为32位,64位系统上长度为64位。
为了在有限的空间存储较多的信息,其数据格式不固定,数据位共享复用。
当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;
当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;
当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。
偏向锁
研究发现,大多数情况下不仅不存在锁竞争,而且总是由同一个线程多次重入,为了让同一个线程多次重入获取锁的代价更低,就引入了偏向锁的概念。
偏向锁获取过程:
- 当前线程访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认是否为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致当前获得偏向锁的线程被暂停)
- 执行同步代码。
偏向锁的释放:
偏向锁只有当遇到其他线程也尝试获取偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。(也就是对于获取偏向锁的线程 只有lock的动作,没有unlock的动作,这是因偏向的需要,即使可能这个线程已经死亡。)偏向锁的撤销步骤如下:
- 等到全局安全点(在这个时间点上没有正在执行的字节码)。
- 暂停持有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置为无锁状态。争抢锁的线程会再走生成偏向锁的过程,然后成为偏向锁的拥有者。
- 如果持有偏向锁的线程还处于活动状态,则将锁升级为轻量级锁。
轻量级锁
轻量级锁实现是自旋锁,没有抢到锁的线程将自旋,即不停的循环判断是否能获取到锁,不会让线程阻塞。获取锁的操作就是通过CAS修改对象头的锁标记位。
长时间的循环是很占用CPU的,一个线程持有锁,其他线程只能原地空耗CPU,不执行任何有效的任务,这种现象称为busy-waiting。
利用短时间的busy-waiting换取线程的阻塞和唤醒在用户态和内核态切换的开销。
busy-waiting是有限度的(JVM可以设置参数调节循环次数上限),如果锁竞争激烈,自旋超过一定次数,就升级为重量级锁。
具体的CAS过程:
- 当线程请求锁时,若该锁对象的Mark Word中标志位为01(未锁定状态),则在该线程的栈帧中创建一块名为『锁记录』的空间,然后将锁对象的Mark Word拷贝至该空间;最后通过CAS操作将锁对象的Mark Word指向该锁记录;
- 若CAS操作成功,则轻量级锁的上锁过程成功;
- 若CAS操作失败,再判断当前线程是否已经持有了该轻量级锁;若已经持有,则直接进入同步块;若尚未持有,则表示该锁已经被其他线程占用,此时轻量级锁就要膨胀成重量级锁。
轻量级锁比重量级锁性能更高的前提是:
- 在轻量级锁被占用的整个同步周期内,不存在其他线程的竞争。若在该过程中一旦有其他线程竞争,那么就会膨胀成重量级锁,从而除了使用互斥量以外,还额外发生了CAS操作,因此更慢!
轻量级锁是为了在线程交替执行同步块时提高性能。
重量级锁
阻塞所有竞争锁但未获得锁的线程。
实现依赖于操作系统的同步函数,在linux上使用mutex互斥锁,涉及到用户态和内核态的切换、进程线程上下文的切换,成本较高。