0%

synchronized关键字

synchronized关键字

synchronized作为java的一个关键字,用来保证同一时刻最多只有一个线程执行synchronized修饰的方法/代码块,保证线程安全,解决多线程并发问题。悲观锁

synchronized的使用

1
2
3
4
5
6
7
8
锁当前对象  synchronized(this){

}

等价于 锁整个方法
synchronized method(){

}
1
2
3
4
5
6
7
锁当前类 synchronized(T.class){

}
等价于 锁静态方法
synchronized static method(){

}

synchronized原理

  • 依赖于JVM monitorenter和monitorexit两个字节码指令,在执行monitorenter指令时,首先需要尝试获取对象锁,如果获得锁,把锁的计数器加一;在执行monitorexit指令时,会将锁计数器减一,当计数器为0时,锁被释放
  • 底层通过一个监视器对象(monitor)完成,wait()、notify()等方法也依赖于monitor对象,monitor(监视器锁)本质依赖于底层操作系统的互斥锁(Mutex Lock)实现,只有拥有锁标记时才能访问这个资源
  • 锁标记存放在java对象头的Mark Word(标记字段)中

早期的JDK中,synchronized是重量级的,需要去操作系统申请,完全依赖于操作系统内部的互斥锁,需要进行用户态到内核态的切换

在JDK1.6进行了锁升级,分别为偏向锁、自旋锁(轻量级锁)、重量级锁

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69



while(true){
synchronized (this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号"+ticket--);
} else {
break;
}
}

}
可以看到,在synchronized同步块操作时会有monitorenter和monitorexit



0 aload_0
1 dup
2 astore_1
3 monitorenter
4 aload_0
5 getfield #2 <com/zhanghe/study/thread/WindowSynchronized.ticket>
8 ifle 72 (+64)
11 ldc2_w #3 <100>
14 invokestatic #5 <java/lang/Thread.sleep>
17 goto 25 (+8)
20 astore_2
21 aload_2
22 invokevirtual #7 <java/lang/InterruptedException.printStackTrace>
25 getstatic #8 <java/lang/System.out>
28 new #9 <java/lang/StringBuilder>
31 dup
32 invokespecial #10 <java/lang/StringBuilder.<init>>
35 invokestatic #11 <java/lang/Thread.currentThread>
38 invokevirtual #12 <java/lang/Thread.getName>
41 invokevirtual #13 <java/lang/StringBuilder.append>
44 ldc #14 <售票,票号>
46 invokevirtual #13 <java/lang/StringBuilder.append>
49 aload_0
50 dup
51 getfield #2 <com/zhanghe/study/thread/WindowSynchronized.ticket>
54 dup_x1
55 iconst_1
56 isub
57 putfield #2 <com/zhanghe/study/thread/WindowSynchronized.ticket>
60 invokevirtual #15 <java/lang/StringBuilder.append>
63 invokevirtual #16 <java/lang/StringBuilder.toString>
66 invokevirtual #17 <java/io/PrintStream.println>
69 goto 77 (+8)
72 aload_1
73 monitorexit
74 goto 90 (+16)
77 aload_1
78 monitorexit
79 goto 87 (+8)
82 astore_3
83 aload_1
84 monitorexit
85 aload_3
86 athrow
87 goto 0 (-87)
90 return

对象头

对象头中主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)

  • Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化

  • Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

对于原子性和可见性的支持

synchronized不仅可以进行同步互斥,还有内存可见性的功能,当一个线程修改了对象状态后,其他线程可以看到该变化

  • 在JMM中规定,对一个变量执行unlock之前,必须先把变量同步到主内存,以此保证了可见性
  • 在JMM中规定,如果对一个变量执行lock操作,将清空工作内存中此变量的值,在执行引擎使用该变量时,需要重新执行load或assign操作初始化变量的值

锁升级

在java6中为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,使得在synchronized中,锁存在四种状态,分别为无锁偏向锁轻量级锁重量级锁,会随着竞争情况逐渐升级,只可以升级不会降级

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,如CAS操作,线程会不断的尝试修改共享资源

对象头的Mark Word内容存储:对象的hashcode、对象分代年龄、是否是偏向锁(0) ,标志位01

偏向锁

一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价,适用于只有一个线程访问同步块的场景,加锁和解锁不需要额外的消耗,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可

对象头的Mark Word内容存储:偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) ,标志位01

偏向锁原理

对于同一个线程多次获得锁,使用偏向锁。一个线程在访问加了synchronized修饰的代码块时,会在对象头中存储当前线程的id,之后该线程进入和退出时,不需要再次加锁和释放锁,而是直接比较对象头里是否存储了指向当前线程的偏向锁

使用偏向锁逻辑

获取偏向锁

  • 首先获取锁对象的Markword,判断是否处于可偏向状态(biased_lock=1、且ThreadId为空)

  • 如果是可偏向状态,则通过CAS操作,把当前线程的ID写入到Markword,如果成功,获得偏向锁,CAS失败,表示当前锁存在竞争,需要撤销已获得偏向锁的线程,升级为轻量级锁

  • 如果是已偏向状态,检查markword中存储的ThreadId是否为当前线程ThreadId,如果不是,说明当前锁已经偏向于其他线程,需要撤销,并升级为轻量级锁

撤销偏向锁

  • 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,这时会把对象头设置成无锁状态并且争抢锁的线程可以基于CAS重新偏向

  • 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,把原获得偏向锁的线程升级为轻量级锁,继续执行代码块

可以使用jvm参数 UseBiasedLocking来设置是否使用偏向锁

轻量级锁

当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能,同步块执行速度非常快,竞争的线程不会阻塞,但是如果始终得不到竞争的线程,使用自旋会消耗CPU

对象头的Mark Word内容存储:指向栈中锁记录的指针,标志位00

轻量级锁加锁逻辑
  • 线程在栈帧中创建锁记录(Lock Record),用于存储对象目前的Mark Word的拷贝

  • 将锁对象的对象头中Mark Word复制到线程创建的锁记录中

  • 虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将LockRecord中Owner指针指向对象的Mark Word

  • 对象Mark Word的锁标志位设置为00

轻量级锁在加锁过程中,用到了自旋锁

锁在自旋的时候会消耗cpu,一直在for循环,默认情况下是自旋10次,可以使用jvm参数preBlockSpin来修改

轻量级锁解锁

解锁是获得锁的逆向逻辑,通过CAS操作把线程栈帧中的LockRecord替换到对象的Markword中,如果成功表示没有竞争,如果失败,表示当前锁存在竞争,轻量级锁会成为重量级锁。

重量级锁

当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁,同步块执行时间较长,线程竞争不使用自旋,不会消耗CPU,但是会导致线程阻塞,响应时间缓慢

对象头的Mark Word内容存储:指向互斥变量的指针,标志位10

重量级基本原路

使用monitorenter/monitorexit获取和释放monitor监视器,使得其它被阻塞的线程可以尝试去获得这个监视器monitor,依赖于操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后进入内核(Linux)调度状态,会导致系统在用户态和内核态之间来回切换,严重影响锁的性能。