本文讲述Synchronized关键字的使用和底层原理,我们使用Synchronized主要是为了保护共享资源在多线程修改的时候,会出现相互覆盖的问题,导致数据错乱。
synchronized关键字用在方法级别,也可以用在方法代码上,用在方法代码块或方法级别时,可以作用于对象或者类,如下所示。
//用在方法上,类级别
public static synchronized void inc(){
a++;
}
//用在方法上,对象级别
public synchronized void inc(){
a++;
}
public void inc2(){
System.out.println("我是java小面");
//synchronized作用于对象
synchronized (lock) {
a++;
}
}
public void inc3(){
System.out.println("我是java小面");
//synchronized作用于类
synchronized (SynchronizedTest.class) {
a++;
}
}
}
用在方法级别时:
用在方法代码块时:
接下来讲解一下Synchronized的底层原理,jdk1.6之前,Synchronized锁是用操作系统的Mutex Lock来实现的,每次加锁和解锁操作都需要用户态到内核态的切换,切换代价是十分高的,导致1.6之前Synchronized称为重量锁;1.6之后使用了各自优化,使得Synchronized锁的性能得到了很大的提升跟reentrantlock是一样的,我们来一起看一下Synchronized的优化原理吧。
Synchronized的锁的级别:偏向锁,轻量锁,重量锁 Synchronized的锁时存储在对象头里面的,所以我们先来看一下对象内存分布:对象头+ 实例数据 + 对齐填充 对象头信息数据结构:Mark Word + 对象类型指针 + [数组长度] Mark Word存储锁的相关信息,结构如下:
偏向锁:简单来说就是把线程ID设置到mark world里,适用于只有一个线程获取锁的场景。 当一个线程Synchronized加锁是,先查看一下对象mark world的标志位是否01,是就进行CAS把线程ID设置到设置到mark world,下次进入同步块时,只需要判断一下mark world是否设置的是当前线程ID,如果是直接进入。可以看到偏向锁只需要一次CAS操作,后续同一个线程只需要判断一下,对于只有没有线程竞争的同步代码块来说,提升的性能是非常可观的。
偏向锁撤销:当有别的线程竞争进入同步代码块时,就需要把偏向锁先撤销;在安全点才会执行这个操作,安全点的时候,线程都会暂停。这时候会两种情况
如果Mark world锁标记不是01,就会直接进行轻量锁的竞争;或者当偏向锁已被其他线程占用时,就会把偏向锁先撤销,然后再升级为轻量锁。轻量锁是指加锁和解锁都需要一次CAS操作,对不同的线程交替获取锁的场景,不同线程获取锁时间时不会有冲突的时候,性能也是非常高的 过程:
image.png
升级为重量锁后,获取锁是操作系统的Mutex Lock,需要进行用户态和内核态的相互切换,性能会收到很大的影响。获取不到锁的线程会阻塞等待,直到其他线程释放锁。
自旋锁:当获取不到锁时,会适当的进行尝试多次获取锁,但是会占用CPU 自适应自旋:会结合上次自旋是否成功获取锁的情况,智能设置自旋次数和自旋时间,以及是否使用自旋。
编译器会自动把一些无用的锁消除掉,比如下面代码:
public void unNeedlock(){
//每次都新建一个对象,相当于没有锁,编译器会自动删除该锁
Object o = new Object();
synchronized (o) {
a++;
}
}
如果一段代码连续操作都对同一个对象反复加锁和 解锁,甚至在循环体中出现了加锁操作是,那即使没有线程竞争,频繁地进行互斥同步操作也会带来性能损耗。编译器会自动把锁方位粗化到循环体外。
public void unNeedlock2(){
for(int i=0;i<100;i++) {
synchronized (lock) {
a++;
}
}
}
//优化后,只需加锁和解锁一次
public void unNeedlock2(){
synchronized (lock) {
for(int i=0;i<100;i++) {
a++;
}
}
}
我们讲解了synchronized关键字的使用和它的底层实现,怎么进行锁升级以及编译器对它的一些优化,你学会了吗?