前言
相信很多同学对synchronized的使用上不陌生,之前也给大家讲解过它的使用。本篇主要带大家深入了解一下它,大家也可以自己试着总结一下,这也是面试中常常问到的,单纯的回答它的基本使用,是惊艳不到面试官的~。
synchronized 介绍
从字面意思翻译过来就是同步的意思,所以它也叫同步锁,我们通常会给某个方法或者某块代码加上Synchronized锁来解决多线程中并发带来的问题,它也是最常用,最简单的一种方法。
在Java中,锁基本上都是基于对象而言的,所以又称为对象锁, 一个类通常只有一个class对象和n个实例对象,它们共享class对象,而我们有时候会对class对象加锁,所以又称为class对象锁。
这里大家要注意的是对象需要是一个非null的对象,我们通常也叫做对象监视器(Object Monitor)。
重量级锁
在JDK 1.5之前,它是一个重量级锁,我们通常都会使用它来保证线程同步。在1.5的时候还提供了一个Lock接口来实现同步锁的功能,我们只需要显式的获取锁和释放锁。
重在哪❓
在1.5的时候,Synchronized它依赖于操作系统底层的Mutex Lock实现,每次释放锁和获取锁都会导致用户态和内核态的切换,从而增加系统性能的开销,当出现大并发的情况下,锁竞争会比较激烈,性能显得非常糟糕,所以称为重量级锁,所以大家往往会选择Lock锁。
锁优化
但是Synchronized又是那么的简单好用,又是官方自带的,怎么可能放弃呢?所以在1.6之后,引入了大量的锁优化,比如自旋锁,轻量级锁, 偏向锁等,下面我们逐个看一下。
synchronized 实现原理
我们了解锁优化之前,我们先看一下它的实现原理。
首先我们看下同步块中,因为它是关键字,我们看不到源码实现,所以只能反编译看一下,通过 javap -v **.class。
public static void main(String[] args) {
synchronized(Demo.class) {
System.out.println("hello");
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/thread/base/Demo
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String hello
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
我们重点关注monitorenter和monitorexit,那么他俩是什么意思呢?
monitorenter,如果当前 monitor 的进入数为 0 时,线程就会进入 monitor,并且把进入数 + 1,那么该线程就是 monitor 的拥有者 (owner)。如果该线程已经是 monitor 的拥有者,又重新进入,就会把进入数再次 + 1。也就是可重入。
monitorexit,执行 monitorexit 的线程必须是 monitor 的拥有者,指令执行后,monitor 的进入数减 1,如果减 1 后进入数为 0,则该线程会退出 monitor。其他被阻塞的线程就可以尝试去获取 monitor 的所有权。指令出现了两次,第 1 次为同步正常退出释放锁;第2次为发生异步退出释放锁。
我们再来看一下, 修饰实例方法中的表现:
class Demo {
public synchronized void hello() {
System.out.println("hello");
}
}
public synchronized void hello();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 25: 0
line 26: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/thread/base/Demo;
}
我们重点关注ACC_SYNCHRONIZED,它作用就是一旦执行到这个方法时,就会先判断是否有标志位,如果有,就会先尝试获取 monitor,获取成功才能执行方法,方法执行完成后再释放 monitor。在方法执行期间,其他线程都无法获取同一个 monitor。归根结底还是对 monitor 对象的争夺,只是同步方法是一种隐式的方式来实现。
synchronized 在 JVM 里的实现就是基于进入和退出 monitor 来实现的,底层则是通过成对的 MonitorEnter 和 MonitorExit 指令来实现。
有了以上的认识,下面我们就看看锁优化。
Synchronized中的锁优化
自适应自旋锁
自旋锁,之前我们讲FutureTask源码的时候,有一个内部方法awaitDone(),给大家有介绍过,就是基于它实现的,今天再给大家总结一下。
它的目的是为了避免阻塞和唤醒的切换,在没有获得锁的时候就不进入阻塞,不断地循环检测锁是否被释放。但是,它也有弊端,我们通常来讲,一个线程占用锁的时间相对较短,但是万一占用很长时间怎么办?这样会占用大量cpu时间,这样会导致性能变差,所以在1.6引入了自适应自旋锁来满足这样的场景。
那么什么是自适应自旋锁呢?自旋的次数不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反过来说,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样一来,就能够更好的利用系统资源。
锁消除
锁消除是一种锁的优化策略,这种优化更加彻底,在 JVM 编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。这种优化策略可以消除没有必要的锁,去除获取锁的时间。
锁粗化
如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。意思是将多个连续加锁、解锁的操作连接在一起,扩展成为一个范围更大的锁, 这个应该很好理解。
偏向锁
偏向锁是JDK 1.6引入的,它解决的场景是什么呢?我们大部分使用锁都是解决多线程场景下的问题,但有时候往往一个线程也会存在这样的问题,偏向锁是在单线程执行代码块时使用的机制。
锁的争夺实际上是 Monitor 对象的争夺,还有每个对象都有一个对象头,对象头是由 Mark Word 和 Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为 1,就进入偏向模式,同时会把这个线程的 ID 记录在对象的 Mark Word 中,当同一个线程再次进入时,就不再进行同步操作,大大减少了锁获取的时间,从而提高了性能。
轻量级锁
我们上边提到的偏向锁,在多线程情况下如果偏向锁失败就会升级为轻量级锁, Mark Word 的结构也变为轻量级锁的结构。
执行同步代码块之前,JVM 会在线程的栈帧中创建一个锁记录(Lock Record),并将 Mark Word 拷贝复制到锁记录中。然后尝试通过 CAS 操作将 Mark Word 中的锁记录的指针,指向创建的 Lock Record。如果成功表示获取锁状态成功,如果失败,则进入自旋获取锁状态。
如果自旋锁失败,就会升级为重量级锁,也就是我们之前讲的,会把线程阻塞,需等待唤醒。
重量级锁
它又称为悲观锁, 升级到这种情况下,锁竞争比较激烈,占用时间也比较长,为了减少cpu的消耗,会将线程阻塞,进入阻塞队列。
synchronized就是通过锁升级策略来适应不同的场景,所以现在synchronized被优化的很好,也是我们项目中往往都会使用它的理由。
结束语
本节的内容比较多,大家好好理解,特别是锁的升级策略。本节我们提到了Lock锁,下一节,带大家深入学习一下Java的Lock 。