synchronized简介

synchronized是Java中的关键字,是一种同步锁。保证同一时刻最多只有1个线程执行 被Synchronized修饰的方法 / 代码。Synchronized可以修饰代码块、方法、类,但其本质是在类上上锁。
对于普通同步方法,锁的是当前实例对象。
对于静态同步方法,锁的是当前类的Class对象。
对于同步方法块,锁的是synchronized括号中配置的对象。

synchronized原理之对象信息

ReentrantLock是通过aqs中state管理锁状态,那么synchronized是怎么做到的呢,所有我们猜测是不是synchronized也是通过类似的模式来实现锁的功能,带着这样的疑问我们走进synchronized。

对象内存信息

通过添加如下的依赖,可以打印对象在内存中的信息

<dependency>
			<groupId>org.openjdk.jol</groupId>
			<artifactId>jol-core</artifactId>
			<version>0.10</version>
		</dependency>
public class SynchronizedDemo {
    private char c;
}

	//调用此方法可输出对象内存信息
	System.out.println(ClassLayout.parseInstance(a).toPrintable());

输出如下信息(64位HoSpot):

com.suning.demo.thead.sync.SynchronizedDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           52 c3 00 20 (01010010 11000011 00000000 00100000) (536920914)
     12     2   char SynchronizedDemo.c                         
     14     2        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

可以看出,分为3快
1、object header 对象头,占12bytes
2、实例数据,即我们自己在类中定义的数据,自定义对象中只定义char,占2bytes
3、对齐填充,上图占2bytes,这是因为JVM规范中要求对象大小必须是8的倍数(计算机软硬件要求)

对象头

遇事看文档,上openjdk的文档,有 object header的介绍,具体如下图:

java锁synchronized与lock java锁synchronized原理_Word


大概意思:每个GC管理的堆对象的开始处的公共结构。包括有关堆对象的布局、类型、GC状态、同步状态和标识哈希代码的基本信息。每个对象头的第一部分为mark word,第二部分为klass pointer。

先解释下概念:

1、Mark Word(标记字)主要用来存放同步状态和标识哈希码,可能包含GC状态位、存放该对象的hashCode;

2、Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

我们来看下openjdk源码oop.hpp类,它是所有类的父类,有markWord和Klass属性,所有每个类都会存在这两个属性。

class oopDesc {
  friend class VMStructs;
  friend class JVMCIVMStructs;
 private:
  volatile markWord _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

Mark Word

从Mark Word的解释看,存放同步状态,所以很大可能通过Mark Word内部管理的元素来控制所,下面重点研究下Mark Word。

上面看到oop.hpp类为有类的父类,所有我们看下markWord属性,源码为markWord.hpp类,类上有一部分对象头的注解。

//  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)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused_gap:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused_gap:1   age:4    biased_lock:1 lock:2 (biased object)

以64位虚拟机为例:
unused:25 未使用占用25bit
hash:31 hash占用31bit
unused_gap:1 未使用占用1bit
age:4 分代年龄占用4bit
biased_lock:1 偏向锁占用4bit
lock:2 锁状态占用1bit
总共64bit,也就是8byte,所有上文中SynchronizedDemo对象的内存信息中,上面两行object heade为Mark Word,下面一行为Klass Word。

//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased
//
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used to mark an object
//    [0 ............ 0| 00]  inflating          inflation in progress

同时还有一段注解表示对象状态,共5种状态
1、无状态 初始状态
2、偏向锁
3、轻量级锁
4、重量级锁
5、gc标记
上图注解中,看出根据 偏向锁和锁状态两个字段控制对象状态,具体情况看下图

java锁synchronized与lock java锁synchronized原理_java_02


思考:

1、为何GC分代年龄最大15?因为在对象头中共用4bit存储分代年龄,4bit值0-15

2、对象状态为何需要偏向锁和锁状态共同控制?因为对象状态一共5中状态,而锁状态在内存中2bit存储,也就说最多4中情况,00 01 10 11,无法表达5中状态,所有 0 01表示无锁状态,1 01表示偏向锁,00 10 11表示剩下3中状态。

synchronized锁实现原理

在jdk1.6之前,锁的实现都是重量级锁,即依赖于操作系统,效率很低。在jdk1.6中对锁的实现引入了大量的优化来减少锁操作的开销:

偏向锁:JDK1.6引入。目的是消除数据再无竞争情况下的同步原语。使用CAS记录获取它的线程。下一次同一个线程进入则偏向该线程,无需任何同步操作。

轻量级锁:JDK1.6引入。在没有多线程竞争的情况下避免重量级互斥锁,只需要依靠一条CAS原子指令就可以完成锁的获取及释放。

重量级锁:内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。

适应性自旋:为了避免线程频繁挂起、恢复的状态切换消耗。产生了忙循环(循环时间固定),即自旋。JDK1.6引入了自适应自旋。自旋时间根据之前锁自旋时间和线程状态,动态变化,用以期望能减少阻塞的时间。

锁升级

锁升级:偏向锁–》轻量级锁–》重量级锁

java锁synchronized与lock java锁synchronized原理_sed_03


说明下锁升级过程:

1、线程1进入同步快,发现未有线程持有锁,即synchronized对象的对象头中的持有锁线程指向为空,则直接拿到锁,修改MarkWorld中对象状态为偏向锁,对象头中的持有锁线程指向线程1

2、 当线程1再次进入同步快,发现持有锁的线程为自己,直接进入

3、若此时线程2进入同步快,则需要竞争锁,撤销偏向锁,升级为轻量级锁,有线程持有锁时其他线程自旋,当持有锁的线程释放锁,其他线程竞争锁,谁成功将对象头中的持有锁线程写入成功表示竞争到锁

4、若此多个线程同时进入同步快或者线程2自旋超过一定次数,升级为重量级锁,等待的线程会放入队列中,阻塞线程,等待唤醒

总结:
1)只有一个线程进入同步快,偏向锁
2)多个线程交替进入同步快,轻量级锁
3)多线程同时进入同步快,重量级锁