前言
多线程并发是Java语言中非常重要的一块内容,同时也是Java基础的一个难点。说它重要是因为多线程是日常开发中频繁用到的知识,说它难是因为多线程并发涉及到的知识点非常之多,想要完全掌握Java的并发相关知识并非易事。也正因此,Java并发成了Java面试中最高频的知识点之一
一、synchronized基本使用
我们知道volatile关键字可以保证共享变量的可见性和有序性,但并不能保证原子性。如果既想保证共享变量的可见性和有序性,又想保证原子性,那么synchronized关键字是一个不错的选择。
synchronized的使用很简单,可以用它来修饰实例方法和静态方法,也可以用来修饰代码块。值的注意的是synchronized是一个对象锁,也就是它锁的是一个对象。因此,无论使用哪一种方法,synchronized都需要有一个锁对象
1.修饰实例方法
synchronized修饰实例方法只需要在方法上加上synchronized关键字即可。
public synchronized void add(){
i++;
}
此时,synchronized加锁的对象就是这个方法所在实例的本身。
2.修饰静态方法
synchronized修饰静态方法的使用与实例方法并无差别,在静态方法上加上synchronized关键字即可
public static synchronized void add(){
i++;
}
此时,synchronized加锁的对象为当前静态方法所在类的Class对象。
3.修饰静态代码块
synchronized修饰代码块需要传入一个对象。
public void add() {
synchronized (this) {
i++;
}
}
很明显,此时synchronized加锁对象即为传入的这个对象实例。
到这里不是道你是否有个疑问,synchronized关键字是如何对一个对象加锁实现代码同步的呢?如果想弄清楚,那就不得不先了解一下Java对象的对象头了。
二、Java对象头与Monitor对象
在JVM中,对象在内存中存储的布局可以分为三个区域,分别是对象头、实例数据以及填充数据。
- 实例数据 存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐。
- 填充数据 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
- 对象头 在HotSpot虚拟机中,对象头又被分为两部分,分别为:Mark Word(标记字段)、Class Pointer(类型指针)。如果是数组,那么还会有数组长度。对象头是本章内容的重点,下边详细讨论。
1.对象头
在对象头的Mark Word中主要存储了对象自身的运行时数据,例如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID以及偏向时间戳等。同时,Mark Word也记录了对象和锁有关的信息。
当对象被synchronized关键字当成同步锁时,和锁相关的一系列操作都与Mark Word有关。由于在JDK1.6版本中对synchronized进行了锁优化,引入了偏向锁和轻量级锁(关于锁优化后边详情讨论)。Mark Word在不同锁状态下存储的内容有所不同。我们以32位JVM中对象头的存储内容如下图所示。
从图中可以清楚的看到,Mark Word中有2bit的数据用来标记锁的状态。无锁状态和偏向锁标记位为01,轻量级锁的状态为00,重量级锁的状态为10。
- 当对象为偏向锁时,Mark Word存储了偏向线程的ID;
- 当状态为轻量级锁时,Mark Word存储了指向线程栈中Lock Record的指针;
- 当