目录
一、什么是synchronized?
二、synchronized的三大特性
synchronized如何保证原子性?
synchronized如何保证可见性?
synchronized如何保证有序性?
三、synchronized的底层实现
1)对象头
2)偏向锁
3)轻量级锁
4)自旋锁
5)重量级锁
一、什么是synchronized?
关键字,synchronized可以保证方法或者代码块在运行时,同一时刻只有一个线程可以进入到被修饰的代码块中,同时它还可以保证共享变量的内存可见性,Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。
它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,此时synchronized锁住的则是()中的对象。
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,此时synchronized锁住的是方法所在类的对象。
- 修饰一个静态的方法,其作用的范围是整个静态方法,此时synchronized锁住的是类的字节码文件。
- 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,此时synchronized锁住的是类的字节码文件。
注意:对象锁是可以有很多把的但是类锁只有一把,因为对象可以创建多个但是类的字节码文件只有一份。
二、synchronized的三大特性
- 原子性:一个或多个操作要么全部执行成功,要么全部执行失败。
synchronized
关键字可以保证只有一个线程拿到锁,访问共享资源。 - 可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。执行
synchronized
时,会对应执行lock
、unlock
原子操作,保证可见性。 - 有序性:程序的执行顺序会按照代码的先后顺序执行。
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
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: getstatic #2 // <- lock引用 (synchronized开始)
3: dup
4: astore_1 // lock引用 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // <- i
9: iconst_1 // 准备常数 1
10: iadd // +1
11: putstatic #3 // -> i
14: aload_1 // <- lock引用
15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16: goto 24
19: astore_2 // e -> slot 2
20: aload_1 // <- lock引用
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // <- slot 2 (e)
23: athrow // throw e
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
synchronized如何保证原子性?
从反编译的字节码文件中我们可以看到加锁时使用了monitorenter和monitorexit。线程在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程放弃了CPU,但是它并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完,这就保证了原子性。
synchronized如何保证可见性?
synchronized
在修改了本地内存中的变量后,解锁前会将本地内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。
synchronized如何保证有序性?
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序可以提高程序执行的性能,但是代码的执行顺序改变,可能会导致多线程程序出现可见性问题和有序性问题。
as-if-serial语义是指不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么可以认为是单线程执行的。所以可以保证其有序性。
三、synchronized的底层实现
1)对象头
1)以32位的虚拟机为例,Java中一个普通对象的对象头包括MarkWord和KlassWord两部分。synchronized锁对象时就是修改MarkWord中的信息来实现把对象锁住。
Mark Word中存储的数据会随着锁标志位的变化而变化,在32位虚拟机中,不同状态下的组成如下
Normal:无锁状态(01),此时的MarkWord存储的是对象的hash值、分代年龄、偏向锁的状态和当前锁状态。
Biased:偏向锁(01),此时的MarkWord存储的是对象的偏向线程ID、重偏向计数器、分代年龄、偏向锁的状态和当前锁状态。适用于多线程访问对象但不是同时访问的情况。
Lightweight Locked:轻量级锁(00),当多个线程同时竞争同一个对象,此时就不适合用偏向锁了,锁会升级为轻量级锁。在多线程交替执行同步代码块时(轻度竞争),避免使用重量级锁带来的性能消耗。
Heavyweight Locked:重量级锁(10)
2)偏向锁
引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。从偏向锁的MarkWord结构中可以看出,如果对象执行了hashCode()方法,则此对象无法进入偏向锁状态。因为MarkWord中没有地方存储ThreadID
偏向锁的获取流程:
批量重偏向:如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID;当撤销偏向锁达到阈值 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至t2。因为前19次是轻量,释放之后为无锁不可偏向,但是20次后面的是偏向t2,释放之后依然是偏向t2。
批量撤销:当一个偏向锁如果撤销次数到达40的时候就认为这个对象设计的有问题;那么JVM会把这个对象所对应的类所有的对象都撤销偏向锁;并且新实例化的对象也是不可偏向的。
3)轻量级锁
引入轻量级锁的目的:在多线程交替执行同步代码块时(轻度竞争),避免使用重量级带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
轻量级锁的获取流程:
- 首先判断当前对象是否处于一个无锁的状态,如果是,Java虚拟机将在当前线程的栈帧建立一个锁记录(Lock Record),用于存储对象目前的Mark Word的拷贝,如图所示。
- 将对象的Mark Word复制到栈帧中的Lock Record中,并将Lock Record中的Object reference指向当前对象,并使用CAS操作将对象的Mark Word更新为指向Lock Record的指针,如图所示。
- 如果第二步执行成功,表示该线程获得了这个对象的锁,将对象Mark Word中锁的标志位设置为“00”,执行同步代码块。
- 如果第二步未执行成功,需要先判断当前对象的Mark Word是否指向当前线程的栈帧,如果是,表示当前线程已经持有了当前对象的锁,这是一次重入,直接执行同步代码块。如果不是表示多个线程存在竞争,该线程通过自旋尝试获得锁,即重复步骤2,自旋超过一定次数,轻量级锁升级为重量级锁。
4)自旋锁
Java锁的几种状态并不包括自旋锁,当轻量级锁的竞争就是采用的自旋锁机制。
什么是自旋锁:当线程A已经获得锁时,线程B再来竞争锁,线程B不会直接被阻塞,而是在原地循环 等待,当线程A释放锁后,线程B可以马上获得锁。
引入自旋锁的原因:因为阻塞和唤起线程都会引起操作系统用户态和核心态的转变,对系统性能影响较大,而自旋等待可以避免线程切换的开销。
自旋锁的缺点:自旋等待虽然可以避免线程切花的开销,但它也会占用处理器的时间。如果持有锁的线程在较短的时间内释放了锁,自旋锁的效果就比较好,如果持有锁的线程很长时间都不释放锁,自旋的线程就会白白浪费资源,所以一般线程自旋的次数必须有一个限制,该次数可以通过参数-XX:PreBlockSpin调整,一般默认为10。
自适应自旋锁:JDK1.6引入了自适应自旋锁,自适应自旋锁的自旋次数不在固定,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果对于某个锁对象,刚刚有线程自旋等待成功获取到锁,那么虚拟机将认为这次自旋等待的成功率也很高,会允许线程自旋等待的时间更长一些。如果对于某个锁对象,线程自旋等待很少成功获取到锁,那么虚拟机将会减少线程自旋等待的时间。
5)重量级锁
当轻量级锁自旋过后还无法获取锁时,对象的锁会膨胀为重量级锁。
重量级锁的获取流程:
1、检查对象头中的MarkWord是否为无锁状态,如果是则通过CAS将Owner指向当前线程
2、如果是有锁状态,则向系统申请一个Monitor对象并与对象进行绑定,完成后进入EntryList中等待。