synchronized原理

  在多线程并发编程中synchronized一直是元老级角色,我们在开发过程中可以使用它来解决线程安全问题中提到的原子性,可见性,以及顺序性。很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了,Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

synchronized的三种应用方式:

  synchronized有三种方式来加锁,分别是:方法锁,对象锁synchronized(this),类锁synchronized(Demo.Class)。其中在方法锁层面可以有如下3种方式:

1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized括号后面的对象:

  synchronized扩号后面的对象是一把锁,在java中任意一个对象都可以成为锁,简单来说,我们把object比喻是一个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的,并且只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以,synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的。

synchronized的字节码指令:

  先看 demo 程序:

public class Demo {
	private static int count = 0;

	public static synchronized void inc() {
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			count++;
	}

	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 1000; i++) {
			new Thread(() -> Demo.inc()).start();
		}
		Thread.sleep(3000);
		System.out.println("运行结果" + count);
	}
}

  通过javap -v 来查看对应代码的字节码指令:

  又看到了熟悉的东西:ACC_SYNCHRONIZED。对于同步块的实现使用了monitorenter和monitorexit指令:他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。

synchronized的锁的原理:

  jdk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁;了解synchronized的原理我们需要明白3个问题:

1.synchronized是如何实现锁

2.为什么任何一个对象都可以成为锁

3.锁存在哪个地方?

  在了解synchronized锁之前,我们需要了解两个重要的概念,一个是对象头、另一个是monitor。

Java对象头:

  在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键

Mawrk Word:

  Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),下面就是对象头的一些信息:

在源码中的体现:

  如果想更深入了解对象头在JVM源码中的定义,需要关心几个文件,oop.hpp/markOop.hpp 。

  oop.hpp,每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应。先在oop.hpp中看oopDesc的定义:

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;//理解为对象头
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass; //默认开启压缩
  } _metadata;
......

  _mark 被声明在 oopDesc 类的顶部,所以这个 _mark 可以认为是一个 头部, 也就是上面那个图种提到的头部保存了一些重要的状态和标识信息,在markOop.hpp文件中有一些注释说明markOop的内存布局:

// Bit-format of an object header (most significant first, big endian layout below):
//
//  32 bits://对应的上图的头部信息的分布
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits: // 64为虚拟机中的分布
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//

  Java 中提供了一个jar包给我来查看对象头信息:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

  然后编写一个测试类:

// oop.hpp  源码-XX:-UseCompressedOops  关闭压缩指针
//打开偏向锁  -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) {
  ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo();
  synchronized (classLayoutDemo) {
    System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
  }
}

  默认情况下输出以下信息:

  这里涉及到大端存储跟小端存储的概念:大端存储与小端存储模式主要指的是数据在计算机中存储的两种字节优先顺序。小端存储指从内存的低地址开始,先存储数据的低序字节再存高序字节;相反,大端存储指从内存的高地址开始,先存储数据的高序字节再存储数据的低序字节。这里正是用这种规则来存储的。

  可以看到这里的是经过压缩的,因为上面中提到 JVM虚拟机中 _compressed_klass 默认开启压缩。可以通过 -XX:-UseCompressedOops 关闭压缩指针。然后打印出来的对象头就是128位的。

  在上面JVM源码中对对象头的描述中,在64位虚拟机的布局中,最后的两位标识锁的类型,倒数第三位标识是否获得偏向锁,因为JDK1.8中默认是关闭偏向锁的 ,这里看到的是轻量级锁的标识,要知道,偏向锁是用来在没有线程竞争的时候减少性能开销通过自旋获得锁,但是实际的场景下还是存在线程的竞争的,所以默认是关闭的。但是我们可以通过 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 开启偏向锁,然后打印出来的数据就是这样的:

  因为我们这里没有竞争,如果存在竞争,就会有不一样的输出 就比如最后3位为  010 ,表示重量级锁

  另外需要注意的是在偏向锁下,对象头内是没有空间存储对象的 哈希码的,那么这个时候我们修改一下Demo

public static void main(String[] args) {
  ClassLayoutDemo classLayoutDemo = new ClassLayoutDemo();
  synchronized (classLayoutDemo) {
    System.out.println(classLayoutDemo.hashCode());
    System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
  }
}

  这个时候输出的信息锁的类型变成了重量级锁了。

Monitor:

  什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制。所有的Java对象是天生的Monitor,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。从源码层面看一下monitor对象

  Ø oop.hpp下的oopDesc类是JVM对象的顶级基类,所以每个object对象都包含markOop

class oopDesc {//顶层基类
  friend class VMStructs;
 private:
  volatile markOop  _mark;//这也就是每个对象的mark头
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

  Ø markOop.hpp 中 markOopDesc继承自oopDesc,

  并扩展了自己的monitor方法,这个方法返回一个ObjectMonitor指针对象:这个ObjectMonitor 其实就是对象监视器

  Ø objectMonitor.hpp,在hotspot虚拟机中,采用ObjectMonitor类来实现monitor:

  到目前位置,对于锁存在哪个位置,我们已经清楚了,锁存在于每个对象的 markOop 对象头中.对于为什么每个对象都可以成为锁呢? 因为每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应,而对应的 oop/oopDesc 都会存在一个markOop 对象头,而这个对象头是存储锁的位置,里面还有对象监视器,即ObjectMonitor,所以这也是为什么每个对象都能成为锁的原因之一。那么 synchronized是如何实现锁的呢?

synchronized是如何实现锁:

  了解了对象头以及monitor以后,接下来去分析synchronized的锁的实现,就会相对简单了。前面讲过synchronized的锁是进行过优化的,引入了偏向锁、轻量级锁;锁的级别从低到高逐步升级, 无锁->偏向锁->轻量级锁->重量级锁.锁的类型:锁从宏观上分类,分为悲观锁与乐观锁。

乐观锁:

  乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁:

  悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

自旋锁(CAS):

  自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起。看持有锁的线程是否能够很快释放锁。怎么自旋呢?其实就是一段没有任何意义的循环。虽然它通过占用处理器的时间来避免线程切换带来的开销,但是如果持有锁的线程不能在很快释放锁,那么自旋的线程就会浪费处理器的资源,因为它不会做任何有意义的工作。所以,自旋等待的时间或者次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。JDK1.6中-XX:+UseSpinning开启; -XX:PreBlockSpin=10 为自旋次数; JDK1.7后,去掉此参数,由jvm控制;

偏向锁:

  大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。下图就是偏向锁的获得跟撤销流程图:

  当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成01(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。执行同步块。这个时候线程2也来访问同步块,也是会检查对象头的Mark Word里是否存储着当前线程2的偏向锁,发现不是,那么他会进入 CAS 替换,但是此时会替换失败,因为此时线程1已经替换了。替换失败则会进入撤销偏向锁,首先会去暂停拥有了偏向锁的线程1,进入无锁状态(01).偏向锁存在竞争的情况下就回去升级成轻量级锁。

开启:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -client -Xmx1024m -Xms1024m

关闭:-XX:+UseBiasedLocking -client -Xmx512m -Xms512m

轻量级锁:

  引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,下面是轻量级锁的流程图:

  在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这个时候 JVM会尝试使用 CAS 将 mark Word 更新为指向栈帧中的锁记录(Lock Record)的空间指针。并且把锁标志位设置为 00(轻量级锁标志),与此同时如果有另外一个线程2也来进行 CAS 修改 Mark Word,那么将会失败,因为线程1已经获取到该锁,然后线程2将会进行 CAS操作不断的去尝试获取锁,这个时候将会引起锁膨胀,就会升级为重量级锁,设置标志位为 10.

  由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程进入等待。 

重量级锁:

  重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。这就是说为什么重量级线程开销很大的。

  monitor这个对象,在hotspot虚拟机中,通过ObjectMonitor类来实现 monitor。他的锁的获取过程的体现会简单很多。每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。

  这里提到的 CXQ跟 EnterList 是什么呢? 见下图:

  这里我们重新回到 objectMonitor.cpp 这个源码中来看以下:

void ATTR ObjectMonitor::enter(TRAPS) {//获取重量级锁的过程
  // The following code is ordered to check the most common cases first
  // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
  Thread * const Self = THREAD ;
  void * cur ;

  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;//进行CAS自旋操作
  if (cur == NULL) {
     // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     // CONSIDER: set or assert OwnerIsThread == 1
     return ;
  }
  //自旋结果相等,则重入(重入的原理)
  if (cur == Self) {
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions ++ ;
     return ;
  }
//接下去就是有并发的情况下竞争的过程了 ....

  所以这就是synchronized实现锁的一个过程。

 wait和notify的原理:

  调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会通知等待线程可以醒了,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。

  看一下 JVM 源码中的逻辑,在objectMonitor.cpp 中:在我们Java代码层面调用的 wait() 方法后,其实在 JVM 层面所作的是,封装  ObjectWaiter 对象并将其放入 _WaitSet 队列,并调用 park()将线程挂起。

// Wait/Notify/NotifyAll
// Note: a subset of changes to ObjectMonitor::wait()
// will need to be replicated in complete_exit above
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
   Thread * const Self = THREAD ;
   assert(Self->is_Java_thread(), "Must be Java thread!");
   JavaThread *jt = (JavaThread *)THREAD;
   DeferredInitialize () ;
   // Throw IMSX or IEX.
   CHECK_OWNER();//检查objectMonitor对象是否指向本线程(即是否获得锁)
   // ... 省略中间的代码
// create a node to be put into the queue // Critically, after we reset() the event but prior to park(), we must check // for a pending interrupt. // 封装了一个ObjectWaiter对象 ObjectWaiter node(Self); node.TState = ObjectWaiter::TS_WAIT ; Self->_ParkEvent->reset() ; OrderAccess::fence();//内存屏障 // ST into Event; membar ; LD interrupted-flag // Enter the waiting queue, which is a circular doubly linked list in this case // but it could be a priority queue or any data structure. // _WaitSetLock protects the wait queue. Normally the wait queue is accessed only // by the the owner of the monitor *except* in the case where park() // returns because of a timeout of interrupt. Contention is exceptionally rare // so we use a simple spin-lock instead of a heavier-weight blocking lock. //将ObjectWaiter放入 _WaitSet中 Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ; AddWaiter (&node) ; Thread::SpinRelease (&_WaitSetLock) ; if ((SyncFlags & 4) == 0) { _Responsible = NULL ; } intptr_t save = _recursions; // record the old recursion count _waiters++; // increment the number of waiters _recursions = 0; // set the recursion level to be 1 exit (true, Self) ; // exit the monitor guarantee (_owner != Self, "invariant") ; //.....省略中间代码 // The thread is on the WaitSet list - now park() it. // On MP systems it's conceivable that a brief spin before we park // could be profitable. // TODO-FIXME: change the following logic to a loop of the form // while (!timeout && !interrupted && _notified == 0) park() int ret = OS_OK ; int WasNotified = 0 ; { // State transition wrappers OSThread* osthread = Self->osthread(); OSThreadWaitState osts(osthread, true); { ThreadBlockInVM tbivm(jt); // Thread is in thread_blocked state and oop access is unsafe. jt->set_suspend_equivalent(); if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) { // Intentionally empty } else if (node._notified == 0) { if (millis <= 0) { // 调用park()将线程挂起 Self->_ParkEvent->park () ; } else { ret = Self->_ParkEvent->park (millis) ; } }     ....... }

   接下去看看 notify 的操作:

void ObjectMonitor::notify(TRAPS) {
  CHECK_OWNER();//同样先检查objectMonitor对象是否指向本线程
  if (_WaitSet == NULL) {//判断wait队列是否为空
     TEVENT (Empty-Notify) ;
     return ;
  }
  DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);

  int Policy = Knob_MoveNotifyee ;
  // 这个 WaitSet - notify 很了然 
  Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
  ObjectWaiter * iterator = DequeueWaiter() ;//dequeue _WaitSet 队列
  if (iterator != NULL) {//不为空,然后接下去就是一系列的判断,最后去唤醒
     //.......
  }
}

wait和notify为什么需要在synchronized里面:

  wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。

  而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。

posted @ 2019-01-11 14:32  吴振照  阅读(10201)  评论(5编辑  收藏