Synchronized关键字

简介
Synchronized用的锁是存在Java对象头里的。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。同步是使用monitorenter和monitorexit指令实现的。monitorenter指令在编译后出入到同步代码块的开始位置,而monitorexit是插入到方法结束处或者异常处,任何一个对象都有一个monitor与之关联,当一个monitor被持有后,它将处于锁定状态。
注意:

  • Synchronized同步块对同一个线程来说是可重入的,不会出现自己把自己锁死的问题;
  • 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

Mutex Lock
监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为”互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙,这就需要从用户态转换到内核态中,因此状态转换需要耗费很多的处理器时间。所以Synchronized是Java语言中的一个重量级操作。不过在JDK1.6中加入了针对锁的优化措施,使得Synchronized和ReentrantLock的性能基本持平,ReentrantLock只是提供了Synchronized更丰富的功能,而不一定有更优的性能,所以在synchronized能实现需求的情况下,优先使用Synchronized来进行同步。

锁优化
Java SE 1.6为了减少获得和释放锁带来的性能消耗,引入了”偏向锁”和”轻量级锁”:锁一共有四个状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
偏向锁、轻量级锁都是乐观锁,重量级锁是悲观锁。

  • 一个对象刚开始实例化的时候,没有任何线程访问它的时候,它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头称为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改为自己的ID,之后再次访问这个对象的时候,只需要对比ID,不需要再使用CAS进行判断操作。
  • 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象是偏向状态,这时表明在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否存活,如果挂了,则可以将对象改为无锁状态,然后重新偏向新的线程。如果原来的线程仍然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,此时轻量级锁由原持有该偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁,如果不存在使用了,则可以将对象恢复成无锁状态,然后重新偏向。
  • 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者稍微等待一下(自旋),两一个线程就会释放锁。但是当自旋超过一定的次数或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀微重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

锁消除
锁消除即删除不必要的加锁操作,虚拟机在即时编辑器运行时,对一些“代码上要求同步,但是被检测到不可能存在共享数据竞争”的锁进行消除。
根据代码逃逸技术:如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

锁粗化
如果一系列的联系操作都是对同一个对象反复的加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有出现想成竞争,频繁的进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机检测到有一串零碎的操作都是对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。

自旋锁和自适应锁

  • 引入自旋锁的原因:互斥同步对性能最大的影响是阻塞的实现,因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。同时虚拟机的开发团队也注意到在许多应用上面,共享数据的锁定状态只会保持很短一段时间,为了这一段很短的时间频繁阻塞和唤醒线程是非常不值得的。
  • 自旋锁:让该线程执行一段无意义的忙循环(自旋)等待一段时间,不会被立即挂起(自旋不放弃处理器执行时间),看持有锁的线程是否会很快释放锁。
  • 自旋锁的特点:自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间,如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程会白白消耗掉持利器的资源,它不会做任何有意义的工作,这样反而会带来性能上浪费。所以自旋等待的时间(次数)必须要有一个限制。默认为10.
  • 自适应的自旋锁:JDK1.6引入自适应的自旋锁,自适应意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。简单说:就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
  • 自旋锁的使用场景:当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。