1.锁
synchronized同步原理
a.synchronized修饰方法时,锁的是当前实例对象;
b.synchronized修饰的是静态同步方法时,锁的是当前对象的class对象;
c.synchronized修饰同步代码块,锁的是synchronized括号里配置的对象。
同步代码块通过monitorenter和monitorexit指令实现,在JVM执行时,会在同步块的区域通过监听器对象去get锁和release锁,从而在字节码层面来控制代码同步.
同步方法和静态同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。JVM根据该修饰符来实现方法的同步。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否已经被设置,如果是,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
Java对象的构成
1.对象头;
2.实例变量;
3.填充数据
实例变量:存储类的属性数据信息,包括父类的属性信息,如果是数组的实例部分,还包含数组的长度。
填充数据:JVM对象的起始地址必须是8字节的整数倍,故为了保证整数倍,才有了填充数据的存在,保证了字节的对齐。
对象头:包含标记字段(mark down)和类型指针(class point),mark down用于存储对象自身的运行时数据,主要有HashCode、GC分代年龄,锁状态标记,偏向锁的线程id,偏向时间戳,轻量级锁等;
对象头存储格式:
长度 | 内容 | 说明 |
---|---|---|
32/64bit | mark down | 存储hashcode和锁信息 |
32/64bit | class metadata address | 存储到对象类型数据的指针 |
32/64bit | list length | 如果当前对象是数组,则表示数组的长度 |
mark down数据格式
32位JVM的mark down数据格式.png
1.1 偏向锁
很多场景中,锁的竞争度是很低的,很多时候总是由同一个线程反复的获取同一个锁。偏向锁针对这种场景而设计,降低了线程获取锁的代价。偏向锁的实现原理:当一个线程访问同步代码块获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该ID的线程在进入和退出同步代码块时不需要CAS操作来加锁和解锁,只需要测试一下对象头的Mark down里是否保存着指向当前线程的偏向锁,如果是,则获得锁;否则,验证Mark down中的偏向锁的标识是否设置为1(1表示当前锁是偏向锁),如果为1,则尝试使用CAS将对象头的偏向锁指向当前线程;否则使用CAS竞争获得锁。
jdk15版本默认情况下禁用偏向锁(主要维护成本比较高),后续是否还支持偏向锁待考虑。
当锁有竞争关系的时候,需要解除偏向锁,进入轻量锁。
1.2 轻量锁
加锁:
线程在执行同步代码块之前,如果同步对象没有被锁定(即锁标记=01),JVM会在当前线程的栈帧中创建用于存储锁记录的空间Lock record,然后线程尝试使用CAS将对象中的mark down替换为指向Lock record的指针,并将对象的mark down状态更新为锁标记=01,则获得轻量级锁成功。
如果CAS更新操作失败了,则检查当前对象的mark down是否指向当前线程的栈帧,如果是说明当前线程已经拥有了该对象的锁,可以直接进入同步块继续执行,否则说明该锁对象已经被其他线程抢占。如果有多个线程同时竞争同一个锁,则轻量级锁升级为重量级锁,锁标识状态为10,mark down中存储的是指向重量级锁的指针,等待线程进入阻塞状态。
解锁:
如果当前对象的mark down仍然指向这线程的锁记录,那就用CAS操作把对象当前的mark down和lock recod中的替换回来,如果成功,则同步过程完成;如果失败,则说明其他线程尝试获得过该锁,要在释放锁的时候,唤起被挂起的线程。
1.3 重量级锁
重量级锁需要操作系统内核支持,需要......
锁的状态转化.jpg锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
加锁和解锁不需要额外的消耗,和执行非同步方法的差距很小 | 适用只有一个线程访问同步代码块的场景 | ||
竞争的线程不会阻塞,提高程序的相应速度 | 最求相应时间,同步代码块执行速度快 | ||
线程竞争不使用自旋,不会消耗CPU | 最求吞吐量,同步块执行时间长的场景 |
1.3 自旋锁
在线程通过互斥同步时,会出现阻塞的情况,会导致线程的挂起和线程的恢复,这两种操作需要在内核态中完成,带来系统的并发性能带来很大压力。而大部分锁定状态持续的时间比较短,线程的挂起和恢复很影响性能。当物理机器有多个CPU时,多个线程并行执行时,自旋锁让后面没有立即获得锁的线程等待一下,即执行一个忙循环(自旋),但不放弃CPU执行时间。如果持有的线程很快释放了锁,那么该线程可以再次竞争锁。当自旋次数太多时,也会白白占用CPU时间,故当线程自旋了n(默认10次,可通过参数-XX:PreBlockSpin设置)次之后,仍然不能够获得锁,那么则让线程挂起。
JDK1.6引入了自适应自旋锁,一个线程自旋的次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
总结:
1.优先使用偏向锁,如果发生线程争用,则升级为自旋锁(乐观锁),自旋10次以后,仍然有线程争用,则升级为重量级锁;
2.锁只可升级,不可降级;
3.通常执行时间短,线程数少,则使用自旋;如果执行时间长,线程数多,适合重量级锁;
4.synchronized是线程可重入锁;
5.synchronized不要使用String,常量,Integer,Long等,因为JVM的原因。
1.4 AQS抽象队列同步器
AQS(Abstract Queued Synchronizer)抽象队列同步器,AbstractQueuedSynchronizer.class位于java.util.concurrent.locks包,它基于CLH(Craig,Landin,and Hagersten,一个虚拟的双向FIFO队列)队列,用volatile修饰共享变量state,线程使用乐观锁CAS (V,A,B)去修改state,成功则获取锁成功,失败则进入等待队列,在等待被唤醒时,使用自旋(while(!CAS (V,A,B)))的方式,不停地尝试获取锁,直到获取成功。常见的具体实现然后有:ReentrantLock(独占锁)、Semaphore(共享锁)、CountDownLatch(共享锁)、ReadWriteLock(共享锁),CyclicBarrier(共享锁)等。
网友评论