synchronized实现方式及锁优化
简述:关于synchronized的具体使用方式,网上已经给出了很多文章,各种不同,主要还是对于锁的对象不同导致,说白了,就是锁一个对象的实例,还是锁一个类,这两个是synchronized在使用时,尤其要关注的。这篇日志主要是记录synchronized其底层实现,以及锁的优化。
synchronized在修饰代码块时,我们编译出来看到就是monitorenter和monitorexist, 这两个字段我们可以通俗理解下,其实就是:
当执行monitorenter时:
1、如果monitor的进入数为0,当前线程进入monitor,并且将线程数设置成1;
2、如果当前线程已经获取到了锁,只是重新进入,会把当前线程数加1;
3、如果其他线程已经占用了monitor,当前线程只能进入阻塞,直到monitor的线程数为0,尝试再次获取锁。
当执行monitorexist时:
1、判断当前线程是否是对象中拥有锁的线程,如果时,将当前对象的线程数减 1, 知道线程数降低为 0,释放该锁
2、其他被改monitor阻塞的线程,尝试去获取当前monitor;
对于同步方法:
编译后,只是在方法签名处多了一个ACC_SYNCHRONIZED 标识符,jvm是根据这个标识符,进行方法的同步:当方法调用时,调用指令会判断ACC_SYNCHRONIZED访问标识是否被设置了,如果设置了,执行线程会先去获取monitor,获取成功之后执行方法体,执行结束之后释放monitor。在方法执行期间,任何线程都不能再获取monitor对象。
任意线程对object的访问都需要获取到object的监视器,如果获取失败,线程进入同步队列,变为blocked状态,当object的监视器被其他线程所释放之后,阻塞队列中的线程会重新尝试获取监视器,
synchronized所提供的同步方法,还有同步代码块,限制了某个时间内只有一个线程可以进行访问,那么我们可以推到出,在synchronized块和synchronized修饰的方式中,对变量的修改是可以写会到主内存的,happens-before在synchronized中也是失效的,代码会按照正常的顺序执行。synchronized所用到的锁是悲观锁,并且是重量级锁。在实际开发中这个锁的性能并不高。
java在1.6之后对锁进行了优化,我简单记录下常见的几种锁。
准备知识:java对象头
java对象头有两部分组成Mark Word和类型指针
mark word:用来存储对象运行时的数据,比如hashcode,gc 分代年龄,锁状态标志,线程持有的锁,偏向线程id等。
类型指针:指向对象的类元数据,简单理解就是该对象是哪个类的实例。
这篇是关于对象头的详细内容 JAVA对象头信息
偏向锁
引入目的:在没有多线程的竞争的情况下,减少不避免的轻量锁执行路径,轻量锁的获取释放依赖于CAS操作,而偏向锁只需要在置换threadID时,依赖一次CAS原子指令,一旦出现多线程竞争时必须撤销偏向锁,所以撤销偏向锁的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了,所以偏向锁是在只有一个线程执行同步块时提升性能。
偏向锁的获取过程:
1、获取到对象中的mark word ,判断是否是可偏向状态,(是否偏向锁设置成1,锁标志位设置成01)
2、如果是偏向锁,判断javaThread中线程是否为空,如果为空,执行步骤3,如果指向当前线程,执行同步代码块,如果不为你当前线程执行步骤4
3、通过CAS指令将当前对象的javaThread设置成当前线程id,如果执行成功,执行同步代码块;否则进入步骤4
4、如果执行CAS指令失败,表示当前多个线程在竞争锁,到达全局安全点,获得偏向锁的线程被挂起,撤销偏向锁,并升级成轻量级锁,升级完成后,被阻塞在安全点的线程继续执行同步代码块。
偏向锁的释放过程:
1、只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。线程不会主动去释放偏向锁、
2、首先会暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。
3、撤销偏向锁之后,恢复到未锁定或者是轻量级锁状态。
轻量级锁
引入轻量级锁的目的:在多线程交替执行的条件下,避免重量级锁引起的性能消耗,但是如果在某个时刻,多个线程进入临界区,会导致轻量级锁膨胀成重量级锁。
轻量级锁加锁过程:
1、代码进入同步块时,如果当前对象锁的状态是无锁,虚拟机会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头当中的mark word复制到锁记录(Lock record)中,即(displaced mark word)
2、拷贝当前对象的mark word到锁记录(lock record)中
3、线程尝试使用CAS将对象头中的mark word替换为指向锁记录中的指针,如果执行成功进入步骤4
4、如果这个更新动作成功了,那么线程就拥有了该对象的锁,将对象锁标志设置成00,表示对象处于轻量级锁状态
5、如果这个更新动作失败了,会首先检查对象的mark word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了锁,可以直接进入同步块执行,否则说明有多个线程在竞争锁,轻量级锁就会膨胀成重量级锁,锁状态变成10,mark word中就是指向重量级锁的指针,后面等待锁的线程就会阻塞,当前线程采用自选的方式来获取锁。
轻量级锁的释放过程:
1、使用CAS操作将dispalcaed mark word替换回到对象头
2、如果替换成功,表示竞争没有发生
3、如果替换失败,表示当前锁存在竞争,锁就会膨胀成重量锁。就要在释放锁的同时,唤醒被挂起的线程。
重量级锁:
1、从轻量级锁到重量级锁的过程中,是通过自旋的方式来获取;
2、判断当前对象monitor是否为重量级锁,如果是重量级锁,执行步骤3,否则执行步骤4
3、获取对象monitor指针,并返回,结束膨胀过程
4、如果当前锁处在膨胀过程中,说明其他线程也在执行膨胀操作,当前线程就自旋的方式,等待锁膨胀完成,如果其他线程膨胀完成,退出自旋操作。
关于各个锁的优缺点对比:
锁 | 优点 | 缺点 | 适应场景 |
偏向锁 | 加锁和不加锁不需要额外的开销,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行速度较长 |
如果错误,敬请指正~
参考:Java并发编程:Synchronized及其实现原理
《java并发编程艺术》方腾飞