Java中的synchronized关键字

synchronized关键字

synchronized是JVM为我们提供的一个重量级锁。

锁对象与锁class

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

实现原理

对于synchronized的代码块,会在前后加入monitorenter和monitorexit关键字。

在执行monitorenter指令时,会尝试获取对象头(MarkWord)上的锁,如果这个对象没被锁定,或者当前线程已经拥有了对象的锁,锁计数器+1,执行monitorexit指令时会将锁计数器-1,当计数器为0时,锁被释放。如果获取对象锁失败,会进入对象的阻塞队列中等待,直到锁被另一个线程释放。

自旋锁与自适应自旋

​ 在使用synchronized关键字时,最消耗CPU资源的是阻塞的实现,挂起和恢复线程都需要由用户态转入内核态进行。而根据研究发现,共享数据的锁定状态持续时间很短,为了这段时间去挂起和恢复线程并不值得,所以引入了自旋锁。

​ 当线程访问到被锁定的资源时,会让线程执行一个忙循环(自旋),不放弃处理器的执行时间,看看持有锁的线程是否会释放锁。

​ 如果持有锁的线程很短时间就释放了锁,那省去了线程切换和挂起的时间,但是如果线程持有时间过长,自旋的线程只会浪费处理器资源。所以自旋等待必须有限度,默认是10次,可以通过-XX:PreBlockSpin来更改。

自旋锁适应:自旋等待的时间不再固定,如果在同一个锁对象,自旋等待的线程刚刚成功获得过锁,持有锁的线程在运行中,JVM就会认为这次自旋获得锁的几率比较大,会允许自旋等待更长的时间;如果对于某个锁,自旋很少成功获得过,JVM可能会在获得锁时省略掉自旋过程。

##锁消除

对于一个同步块,如果JVM检测在这段代码块里,堆上的变量并不会被其他线程所访问到,就可以把他们当初栈上数据对待,认为它们是私有的,就无需进行加锁。

锁消除的主要判断依据与逃逸分析的数据支持。

逃逸分析的基本行为就是分析对象的动态作用域:如果一个对象在方法中被定义了,可能被外部方法引用,例如作为调用参数传递到其他方法,这叫方法逃逸;也可能被外部线程访问到,例如赋值给类变量作为线程共享,被称为线程逃逸。

如果证明一个对象不会逃逸到方法或者线程之外,也就是别的方法或者线程无法访问到这个对象,可能会对这个对象进行高效的优化:

  • 栈上分配: 栈上分配的对象可以随着栈帧出栈而被销毁,减少了GC的压力。
  • 同步消除: 如果通过逃逸分析能确定一个对象无法被其他线程访问到,那么这个变量的读写就不会产生竞争,就无需对这个对象进行同步。
  • 标量替换: 标量指的是无法再被分解成更小的数据了。Java中的原始数据类型int、long以及reference就是标量。Java对象则是聚合量,可以继续被分解。如果把一个Java对象拆散,将其成员变量恢复至原始类型变量就是标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散,实际运行中可能不会创建这个对象,改为创建它的若干个被这个方法使用到的成员变量替换。这样成员变量可以在栈上分配和读写。

锁粗化

如果一系列的连读操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体里的,那即使没有线程竞争,频繁的进行互斥同步操作也会损耗性能。

如果虚拟机检测到一些零碎的操作都对同一个对象加锁,将会把同步的范围拓展(粗化)到整个操作序列的外部。如:

StringBuffer sb = new StringBuffer();
sb.append("1");
sb.append("2");
sb.append("3");

StringBuffer的append每次都是加锁的,在这个代码块中,会将锁粗化到整个代码块,而不会进行3次加锁解锁。

轻量级锁

轻量级锁是相对利用系统互斥量来实现的传统锁而言的,传统的锁机制就称为“重量级锁”。

要理解轻量级锁,首先看对象的对象头,它一部分存储对象自身的运行时数据,如GC分代年龄、哈希码等,这部分称为“Mark Word”,另一部分存储指向方法区对象类型数据(Class对象)的指针,如果是数组对象,还会额外存储数组的长度。

轻量级锁的执行过程:

  1. 代码进入同步块,首先检查对象是否被锁定。如果没有锁定(锁标志位为01),在当前线程的栈帧中建立一个名为锁记录(Lock record)的空间,用于存储当前对象Mark Word的拷贝,这时线程栈帧与对象头的状态:

  1. 虚拟机用CAS操作,将对象的Mark Word改为指向栈帧中锁记录Lock Record的指针,如果这个更新动作成功了,那么该线程就拥有了对象的锁,并且对象的锁标志位(Mark Word的最后2bit)改为00,代表轻量级锁,这时堆栈跟对象头的状态:

3.  如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前栈帧的Lock Record,如果是就说明当前线程已经拥有了对象的锁,那么可以进入同步块继续执行。否则说明这个锁对象被其他线程占用了。当前线程进行自旋获得锁,如果没有获得锁,就将轻量级锁改为重量级锁,进入阻塞状态。如果有2个以上的线程争用一个锁,那么轻量级锁就不再有效,要膨胀成重量级锁。锁标志变为10,后面等待锁的线程也要进入阻塞状态。
4.  轻量级锁的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向着线程的锁记录,就用CAS操作把对象当前的Mark Word和线程中复制的Mark Word替换回来。如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那么就要在释放锁的同时,唤醒被挂起的线程。

在轻量级锁升级成重量级锁后,不会再降级回去。

##偏向锁

轻量级锁是把加锁的过程改为CAS操作代替Lock,而偏向锁就连CAS操作就不做了。

​ 偏向锁是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,持有偏向锁的线程将永远不需要再进行同步。

​ 如果当前虚拟机启动了偏向锁,在线程第一次获得锁的时候,虚拟机会使用CAS操作将对象头的锁标识位改为01,即偏向模式,并且记录下当前线程的ID。如果CAS操作成功,以后每次进入这个锁相关的同步块,虚拟机都可以不再进行任何同步操作。

​ 偏向锁使用了一种等待竞争出现才释放锁的机制,当另一个线程去使用CAS尝试获取锁时,偏向模式就结束了。将锁对象恢复到无锁或者标记不适合偏向锁。

​ 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值