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的介绍,具体如下图:
大概意思:每个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标记
上图注解中,看出根据 偏向锁和锁状态两个字段控制对象状态,具体情况看下图
思考:
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引入了自适应自旋。自旋时间根据之前锁自旋时间和线程状态,动态变化,用以期望能减少阻塞的时间。
锁升级
锁升级:偏向锁–》轻量级锁–》重量级锁
说明下锁升级过程:
1、线程1进入同步快,发现未有线程持有锁,即synchronized对象的对象头中的持有锁线程指向为空,则直接拿到锁,修改MarkWorld中对象状态为偏向锁,对象头中的持有锁线程指向线程1
2、 当线程1再次进入同步快,发现持有锁的线程为自己,直接进入
3、若此时线程2进入同步快,则需要竞争锁,撤销偏向锁,升级为轻量级锁,有线程持有锁时其他线程自旋,当持有锁的线程释放锁,其他线程竞争锁,谁成功将对象头中的持有锁线程写入成功表示竞争到锁
4、若此多个线程同时进入同步快或者线程2自旋超过一定次数,升级为重量级锁,等待的线程会放入队列中,阻塞线程,等待唤醒
总结:
1)只有一个线程进入同步快,偏向锁
2)多个线程交替进入同步快,轻量级锁
3)多线程同时进入同步快,重量级锁