zhaoyu@home:~$

Synchronized

synchronized使用场景

  • Synchronized修饰普通同步方法:锁对象当前实例对象;
  • Synchronized修饰静态同步方法:锁对象是当前的类Class对象;
  • Synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象,这个对象可以是某个对象(xlock),也可以是某个类(Xlock.class)。

注意:

  • 使用synchronized修饰非静态方法或者代码块指定修饰的对象为实例对象时,同一个类的不同对象拥有自己的monitor,因此不会相互阻塞。
  • 使用synchronized修饰类和对象时,由于类和实例都拥有自己的monitor,因此不会相互阻塞。
  • 一个对象只有一个monitor,所以当一个线程正在访问实例对象的一个synchronized方法时,其它线程不能访问使用该对象monitor的代码。
  • 线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的monitor, 而后者获取的是类对象的monitor,两者不存在互斥关系。

java对象头

java对象在堆内存中的可以分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充区域(可能存在)。对齐填充区域为了让对象大小是8字节的 整数倍。实例数据描述真是的对象数据。而对象头中又有三部分:

  • 数组长度
    表达数组长度,只有数组形式的对象才有
  • klass
    是一个指针区域,指向元数据区中(JDK1.8)该对象所代表的类描述,这样JVM才知道这个对象是哪一个类的实例。
  • markword
    对象关键的运行时数据,主要就是这个对象当前锁机制的记录信息。同时也记录了对象的锁信息。在64为的虚拟机中,如下图所示:

synchronized锁机制升级

Java中对悲观锁思想的实现就是我们最常使用的synchronized同步块。在java1.6前,synchronized被称为重量级锁,java1.6对synchronized进行了 各种优化。减轻了获取锁和释放锁带来的性能开销,引入了偏向锁和轻量级锁。

  1. 偏向锁
    在没有多线程多对象操作权进行抢占的情况下,取消对象的同步操作,唯一请求操作权的线程被记录到对象头中。主要解决只有一个线程访问,没有对象抢占的情况。 HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了 降低获取锁的代价,才引入的偏向锁。偏向锁默认是开启的,可以通过-XX:-UseBiasedLocking = false取消偏向锁。

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较 当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2 要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为 无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前 线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

  1. 轻量级锁
    当出现了两个或多个线程抢占对象操作时,偏向锁就会升级为轻量级锁。轻量级锁同样使用CAS技术进行实现。
    线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS尝试把对象 头中的内容替换为线程1存储锁记录(DisplacedMarkWord)的地址,如果成功,获得锁成功,失败,则使用自旋锁重试。抢占锁的其他线程和线程1的流程一样。

    轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就 被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
    自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次。如果多次CAS持续失败,说明当前对象的多线程抢占现象很严重, 这是对象锁升级为重量锁状态,防止CPU空转,并使用操作系统层面的Mutex Lock(互斥锁)技术进行实现。

为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级, 但是偏向锁状态可以被重置为无锁状态。
如下为各种锁之间的比较:

  1. 重量级锁
    当对象的锁级别升级为重量级锁时,JVM就开始采用Object Monitor机制控制各线程抢占对象的过程了。实际上这是JVM对操作系统级别Mutex Lock(互斥锁) 的管理过程。

Object Monitor(监视器或者管程)

重量级锁即Monitor。每个对象都拥有一个的monitor。下图简单描述了多线程获取锁的过程:

  1. 当多个线程同时访问一段同步代码时,首先会进入 EntryList,当线程获取到对象的 monitor 后进入 Owner 区域并把 monitor 中的 owner 变量设置 为当前线程,同时 monitor 中的计数器count 加1,其他在EntryList中的线程为Blocking状态。
  2. 若线程调用 wait() 方法,将释放当前持有的 monitor,owner变量恢复为 null,count自减1,同时该线程进入 WaitList 集合中等待被唤醒,线程 状态为Waiting。直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入Owner区。
  3. 若当前线程执行完毕也将释放 monitor (锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
    monitor记录了线程的id,当有一个线程进入Owner区,计数器count加1,因此monitor也是可重入锁。