Synchronized详解

1、说明:
  • 随着JavaSE 1.6 对 Synchronized 进行的各种优化后, Synchronized 并不会显得那么重了。
  • Synchronized 可以把任何一个 非 null 对象作为锁,锁有个专门的名字 对象监视器(Object Monitor)
2、Synchronized 的三个作用:
  • 原子性: 确保线程互斥的访问同步代码。
  • 可见性: 保证共享变量的修改能够及时看见,其实是通过 JMM来保证的
    • 对一个变量 unlock 操作之前,必须要同步到主内存中;
    • 如果一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从内存中load操作。
  • 有序性:有效的解决重排序问题,即“一个Unlock操作先行发生(happen-before)于后面对用一个锁的lock操作”
3、Synchronized总共有三种用法:
  • 当 Synchronized 作用在实例方法时,监视器锁(monitor)便是对象实例(this);
  • 当 Synchronized 作用在静态方法时,监视器锁(monitor)便是对象的 Class实例,因为Class 数据存在于永久代,因此静态方法锁相当于该类的一个全局锁。
  • 当 Synchronized 作用在某一个对象实例时,监视器(monitor)便是括号里的对象实例。
4、Synchronized 执行流程:
  • monitorenter: 每一个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程 monitor指令是尝试获取monitor所有权。
  • 过程:
    • 如果monitor进入数为 0,则该线程进入 monitor,然后将进入数+1,该线程即为moniro的所有者
    • 如果该线程已经占用了该monitor,只是重新进入, monitor +1
    • 如果过其他线程已经占有了该 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为0,再重新尝试获取 monitor 的所有权
  • monitorexit: 执行者线程必须是 monitor 的所有者,执行 -1;如果=0,线程将退出 monitor,不在持有该锁
    • monitorexit 指令一般出现两次,一次同步正常退出释放锁,一次发生异常异步退出释放锁。
  • Synchronized 底层是通过 monitor 对象来完成的,wait/notify等方法也依赖于monitor对象, 这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
  • 显示方式: Synchronized 作用于某个对象,字节码会加上 monitorenter 和 monitorexit
  • 隐示方式: Synchronized 作用于某个方法,调用指令会检查 方法的 ACC_SYNCHRONIZED 是否被设置,后续回去 monitor, 两者差不多
5、监视器(Monitor):
  • 在Java 中任何一个对象都有一个 Monitor 与之关联,当且一个Monitor 被持有后,它将处于锁定状态。
  • Monitor是由ObjectMonitor实现的,其主要数据结构如下:
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL; // 线程拥有者
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
  • ObjectMonitor 有两个队列, _WaitSet 和 _Entrylist , 用来保存ObjectWaiter对量列表(每个等待锁的对象都会被封装成 ObjectWaitor 对象)
  • _owner指向持有ObjectMonitor对象的线程
  • 多个线程竞争通一把锁时:
    • 首先进入到 _EntryList 中,当线程获取到对象的 Monitor 后,进入到 _Ower 区域并把 monitor 中的owener 设置为当前线程,count+1【可重入锁】
    • 若线程调用 wait() 方法,将释放当前持有的 monitor ,owner变量恢复为null,复位count,同时线程进入 WaitSet 集合中等待被唤醒。
    • 若当前线程执行完毕,也将释放 monitor(锁) 并复位Count 值,以便其他线程获取锁
  • notify : 会唤醒在 _WaitSet中的一个线程ObjectWaitor,进入到 _EntryList中,等待线程锁。
  • notifyAll :唤醒所有 _WaitSet 中线程,放入到 _EntryList 中,等待获取线程锁。
  • 执行流程:
    • 一个线程通过1号门进入Entry Set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码
    • 如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待。
    • 线程在持有监视器的过程中,有两个选择,一个是正常执行监视器区域的代码,释放监视器,通过5号门退出监视器。
    • 还有可能等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息,直到相应的条件满足后再通过4号门进入重新获取监视器再执行。
      在这里插入图片描述
6、Synchronized 锁 在对象中的存储:
  • 在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充
    • 实例数据:存放类的属性数据信息,包括父类的属性信息;
    • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
    • 对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit)
  • 对象头中,包含 Mark Word 它是实现 轻量级锁和偏向锁 的关键, 其中线程中有私有对象: Lock Record ,
    - 用于Copy Mark Word,每一个被锁住的对象Mark Word都会和一个Lock Record关联
    在这里插入图片描述
6、锁的优化:
  • JDK5 仅仅引入了concurrent包,并没有对synchronized 做优化。
  • JDK6 对 synchronized 使用了JDK5中的CAS自旋,之外还增加了 自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。
    锁主要存在 四种状态: 无锁状态 > 偏向锁状态 > 轻量级锁状态 > 重量级锁状态, 锁是单向升级的,从低到高,不可逆。
6.1、自旋锁:
  • 问题:
    • 线程的阻塞和唤醒 需要从 CPU 从 用户态 切换到 核心态, 频繁切换对CPU 是负担很重的工作,
    • 对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的
  • 自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态
    • 自旋锁 适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。
    • 自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间,锁释放的越快,自旋锁的效率越高
    • 自旋等待的时间(自旋的次数)有一个限度【JDK1.6中默认10次】,如果自旋超过了定义的时间仍然没有获取到锁,则被挂起。
6.2、自适应自旋锁:
  • JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
  • 线程如果自旋成功了,那么下次自旋的次数会更加多
  • 如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
6.3、消除锁:
  • 当JVM 检测到不可能存在共享数据竞争是,JVM会对锁进行消除。
  • 锁消除的依据是逃逸分析的数据支持,
  • 例如,vector局部变量,其中的锁就会被忽略。
public void vectorTest(){
    Vector<String> vector = new Vector<String>();
    for(int i = 0 ; i < 10 ; i++){
        vector.add(i + "");
    }
}
6.4、锁的粗化:
  • 锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免多次切换线程导致系统消耗。
  • 如上面Code:
    • JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外
6.5、偏向锁:
  • 场景:
    • 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获取。为了线程获取锁代价降低,引进了偏向锁。
    • 偏向锁是在单线程执行代码块时使用的机制,多线程下【线程A未执行完,线程B发起了申请锁的申请】,则一定会转换为轻量级锁或重量级锁。
    • 轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的,而偏向锁只需要在置换偏向ThreadID的时候依赖一次CAS原子指令
    • 轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
    • 偏向锁的想法是一旦线程第一次获得了监视对象,不会去释放锁,直到下次有线程来竞争,竞争达到安全点,升级为轻量级锁。
  • 流程:
    • 1、检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
    • 2、若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
    • 3、如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
    • 4、通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
  • 偏向锁通过 CAS 操作是Mark Word 中的偏向线程ID【操作对象头】.
6.6、轻量级锁:
  • 引入的目的: 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
  • 流程:
    • 1、在线程进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
    • 2、拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;
    • 3、拷贝成功后,虚拟机将使用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5
    • 4、 如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,
    • 5、如果这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,进入自旋执行(3),若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。
  • **轻量级锁通过CAS 操作线程栈中的 Lock Record 和 锁对象里的 Mark Word【操作对象头】. **
6.7、重量级锁:
  • Synchronized是通过对象内部的一个叫做 **监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间 **,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”
  • 重量级锁通过 Monitor 对象监视器来实现的
7、锁的优缺点:
优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗CPU追求响应时间。
同步块执行速度非常快
重量级锁线程竞争不适用自旋,不会消耗CPU线程阻塞,相应时间缓慢追求吞吐量
同步块执行时间较长
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值