本文已参与「新人创作礼」活动,一起开启掘金创作之路。
synchronized基础概念
- 要完成原子性操作,只能靠锁来实现
- 上锁的本质就是把并发操作转变为序列化操作
- 修饰方法锁的是对象,修饰静态方法锁的是类的class,class只有一个
- 抛出异常,锁会被释放
- synchronized锁对象时不要用Integer、String、Long类型
- Integer和Long会有自动拆箱和装箱的过程,改变值后,可能不会是原来的对象,同步块就会失效
- String因为有常量池的存在,值一致时,指向的地址会一致,会出现两个线程要锁的是同一个对象
- JDK早期,synchronized上的是重量级锁
- 目前的synchronized包含重量级锁,轻量级锁和偏向锁
- synchronized可以保证可见性和原子性但是不能保证有序性
- synchronized是可重入锁,重入次数必须得记录,解锁时要对应,记录的位置就在MarkWord里面
- synchronized内部自带的轻量级锁和偏向锁,都不用向操作系统打交道,只需要在用户态就可以完成
- 开发中通常选择synchronized,因为目前的synchronized内部做了很多优化,有锁升级的机制,具备自适应功能
- synchronized优化指的就是把锁的粒度变粗或者变细
悲观锁(重量级锁)
- 和锁关联的会有一个队列(WaitSet:重量级锁的队列),让后面的线程在队列中排队
- 线程执行时间长,竞争的线程比较多,用悲观锁,可以不占用CPU资源
乐观锁(轻量级锁,自旋锁,CAS)
- 乐观锁的原理就是执行时不上锁,执行完后比较这个资源是不是执行前的资源
- 如果是,则替换结果,如果不是,则会拿当前资源再去执行一遍,形成自旋过程
- ABA问题:比较资源时,资源A可能之前被改成过B,然后又改回了A,如果资源是引用,则无法知道里面的值是否一样
- ABA解决办法:可以在资源上添加一个版本号解决
- 乐观锁的自旋过程是会消耗CPU资源的
- 线程执行时间短,竞争的线程比较少,再用乐观锁
- Java中用AtomicIntege可以实现乐观锁
- 乐观锁中的比较和交换两个指令必须要是原子操作
- 所以追源码可以发现AtomicIntege底层最终实现的汇编指令是lock cmpxchg
偏向锁
- 偏向锁就是只有一个线程来的时候,由synchronized把这个线程的ID(c++中叫线程指针)写到MarkWord里面
- 单线程的时候,偏向锁没有竞争,所以效率很高
- 当产生竞争时,偏向锁会撤销
- 偏向锁的启动会有一个4秒延迟
- 因为JVM启动时有很多默认线程,里面很多synchronized代码,如果一来就启动偏向锁,会涉及到大量的锁撤销和锁升级操作,效率会很低
synchronized锁升级过程
- 普通对象的情况下,单线程访问,synchronized会使用偏向锁,多线程访问,synchronized会直接使用轻量级锁
- 当对象属于匿名偏向态时,单线程和多线程访问,产生的锁都会为偏向锁
- JDK11中,默认都是匿名偏向态(依然存在4秒延迟),标记为101,JDK8中,默认都是无锁 的普通对象,标记为001
- 当产生竞争时,偏向锁会撤销,线程之间开始竞争,每个线程内部都会生成一个LR(Lock Record 锁记录),此时锁升级为轻量级锁
- 当竞争加剧时,会向操作系统申请资源,linux mutex
- 线程挂起,进入等待队列,等待操作系统调度,然后再映射回用户空间,至此升级为重量级锁
- 竞争加剧:有线程超过10次自旋或者自旋线程数超过CPU核数一半,1.6之后JVM会自己控制
- synchronized锁的升级过程就是通过修改Markword的最低两位来做的升级,倒数第三位是偏向锁的标记位