前言:为什么使用synchronized?
在多线程编程中,一个资源会被多个线程共享,为了避免因为资源抢占导致数据错乱,所以要对线程进行同步。因此,引入synchronized关键字。
以下,来探究下synchronized的使用和底层原理。
一、synchronized的作用
1.1原子性
原子性:指一个操作或多个操作,要么全部执行,要么全部不执行。
java中,赋值和读取值的操作都是原子的。
但是i++,i+=1等操作不是原子的,因为这些操作在底层是不是一个操作,而是被分成了读取,计算,赋值几步操作,所以保证这几步的原子性,才能保证i++的原子性。
注意:synchronized 可以保证原子性,但是volatile不能保证原子性。
1.2 可见性
可见性:指多线程访问同一资源时,该资源对所有线程都是可见的。
synchronized对一个资源加锁之后,在释放锁之前会将变量的修改刷新到主内存中,保证资源的可见的。
1.3 有序性
有序性:指程序中的代码会按照一定的顺序执行。
在java中,有编译器重排,指令级重排以及系统重排,这些指令重排会使指令实际执行的顺序与实际可见的顺序不同。
synchronized保证了指令不会被重排,按照显示的顺序执行。
注:synchronized是可重入的。即一个线程已经获取该资源的synchronized锁时,还可以再次获得该资源的锁。
二、synchronized的作用范围
synchronized关键字可以修饰静态方法,实例函数,代码块。
public class SyncDemo {
// 修饰静态方法,对类加锁
public static synchronized void test1(){
}
// 修饰实例方法,对实例对象加锁
public synchronized void test2(){
//修饰代码块,对类加锁
synchronized (SyncDemo.class){
}
}
public void test3(){
// 修饰代码块,对当前对象,即实例方法加锁
synchronized (this){
}
}
public static void main(String[] args) {
}
}
2.1 修饰静态方法
由类加载机制可以知道,静态方法是和类同时加载的,归属于类。所以当synchronized修饰静态方法时,是对类加锁。
2.2 修饰实例方法
synchronized修饰实例方法 ,即对当前实例方法加锁。
2.3 修饰代码块
如上代码:修饰的第一个代码块是对实例方法加锁,修饰的第二个代码块是对类加锁。所以修饰代码块时,是可以选择加锁对象的。
三、synchronized底层原理
我们将上述代码反编译成字节码,看看底层实现原理。
从class字节码文件可以看出,一个通过方法flags标志,一个是通过monitorenter和monitorexit指令。
3.1 修饰实例方法
可以看出,synchronized修饰实例方法时,是在方法的flags里面加了一个ACC_SYNCHRONIZED标志。
此标志表示JVM这是一个同步方法,该线程进入该方法时,需要先获取对应的锁,且锁计数器加1,释放时减1.
3.2 修饰代码块
从反编译的字节码可以看出,同步代码块是由monitorexit指令进入,然后monitorexit释放锁。
但是截图中可以看出,有两个monitorexit,这里为什么有两个monitorexit?
第二个monitorexit是来处理异常的。正常情况下,第一个monitorexit之后会执行goto指令,而该指令的返回是后面的return。正常情况下只会执行第一个monitorexit释放锁,然后返回。
而如果执行中发生了异常,第二个monitorexit起作用了,它由编译器自动生成,发生异常时处理异常,然后释放锁的。
四、synchronized锁的底层原理实现
上面我们了解了JVM中如何实现synchronized锁的,但是JVM中如何对对象加锁的呢?
JVM中,对象由三部分构成:对象头,实例数据,对齐填充。
先简单介绍下实例数据和对齐填充:
实例数据:存放类的属性数据信息,包括父类的属性信息。如果是数组实例,还包括数组的长度。
对齐填充:不是必需部分,是虚拟机要求对象地址是8字节的整数倍,所以仅仅是用来字节对齐。
对象头:包括两个部分,Mark Word和Class Metadata Address。Mark Word存储对象的hashCode,锁信息,和分代年龄等信息;而后者记录指针指向对象的类元信息,即确定对象是哪个类的实例。
锁的状态
额外说明下锁的状态,在JDK1.6之前,锁只有无锁和重量级锁两个状态。在JDK1.6之后,对synchronized锁进行了优化,锁状态有四个:无锁状态,偏向锁,轻量级锁,重量级锁。
五、synchronized锁的优化
因为JDK1.6之前,只有重量级锁,所以使用synchronized会造成很大的消耗,所以之后,对synchronized锁进行了优化。
5.1 偏向锁
针对使用了synchronized锁,但是实际操作时,不存在锁竞争时,会加上偏向锁。头对象会标记偏向锁的状态位,见上图。
这样就减少了同一线程获取锁的代价。
5.2 轻量级锁
由偏向锁升级而来,当存在第二个锁申请同一个锁对象时,偏向锁就会升级为轻量级锁。
5.3 重量级锁
由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁会倍升级为重量级锁。
5.4 锁升级
锁的状态会一步步升级,无锁——>偏向锁——> 轻量级锁——> 重量级锁。而且锁的升级是不可逆的。即升级到轻量级锁,重量级锁,是无法自动恢复到无锁,偏向锁的状态的。
5.5 锁消除
锁消除是JVM另一种锁优化机制,指编译时,对上下文的分析,去除不可能存在竞争的锁。
5.6 锁粗化
锁粗化也是JVM的一种锁优化机制,通过扩大锁的范围,避免反复的加锁和释放锁。