悲观锁

定义:悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

实现方式:synchronized关键字Lock的实现类都是悲观锁

适用场景:写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源。

伪代码说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 //=============悲观锁的调用方式
public synchronized void m1()
{
//加锁后的业务逻辑......
}

// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
lock.lock();
try {
// 操作同步资源
}finally {
lock.unlock();
}
}

乐观锁

定义:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入;如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。

实现方式(乐观锁在Java中是通过使用无锁编程来实现):

  • 版本号机制Version(只要有人提交了就会修改版本号,可以解决ABA问题):

    • ABA问题:在CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。
    • 解决方法:Juc包提供了一个AtomicStampedReference,原子更新带有版本号的引用类型,通过控制版本值的变化来解决ABA问题。
  • 最常采用的是CAS(Compare-and-Swap,即比较并替换)算法,Java原子类中的递增操作就通过CAS自旋实现的。

伪代码说明:

1
2
3
4
//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

synchronized

三种实现方式

  • 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
  • 作用于代码块,对括号里配置的对象加锁。
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;

从字节码角度分析synchronized实现

文件反编译技巧:

  • 文件反编译javap -c ***.class文件反编译,-c表示对代码进行反汇编
  • 假如需要更多信息 javap -v ***.class ,-v即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)

下面是三种实现方式的case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
Object o = new Object();

public void m1() {
synchronized (o) {
System.out.println("------hello synchronized m1");
}
}

public synchronized void m2() {
System.out.println("------hello synchronized m2");
}

public static synchronized void m3() {
System.out.println("------hello synchronized m3");
}
}

反编译的结果:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
PS D:\javaProjects\JUC\out\production\JUC> javap -v .\Main.class
Classfile /D:/javaProjects/JUC/out/production/JUC/Main.class
Last modified 2022-10-6; size 844 bytes
MD5 checksum 46c192fa8189f98fb511a74313c04ec3
Compiled from "Main.java"
public class Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#28 // java/lang/Object."<init>":()V
#2 = Class #29 // java/lang/Object
#3 = Fieldref #9.#30 // Main.o:Ljava/lang/Object;
#4 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #33 // ------hello synchronized m1
#6 = Methodref #34.#35 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = String #36 // ------hello synchronized m2
#8 = String #37 // ------hello synchronized m3
#9 = Class #38 // Main
#10 = Utf8 o
#11 = Utf8 Ljava/lang/Object;
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 LMain;
#19 = Utf8 m1
#20 = Utf8 StackMapTable
#21 = Class #38 // Main
#22 = Class #29 // java/lang/Object
#23 = Class #39 // java/lang/Throwable
#24 = Utf8 m2
#25 = Utf8 m3
#26 = Utf8 SourceFile
#27 = Utf8 Main.java
#28 = NameAndType #12:#13 // "<init>":()V
#29 = Utf8 java/lang/Object
#30 = NameAndType #10:#11 // o:Ljava/lang/Object;
#31 = Class #40 // java/lang/System
#32 = NameAndType #41:#42 // out:Ljava/io/PrintStream;
#33 = Utf8 ------hello synchronized m1
#34 = Class #43 // java/io/PrintStream
#35 = NameAndType #44:#45 // println:(Ljava/lang/String;)V
#36 = Utf8 ------hello synchronized m2
#37 = Utf8 ------hello synchronized m3
#38 = Utf8 Main
#39 = Utf8 java/lang/Throwable
#40 = Utf8 java/lang/System
#41 = Utf8 out
#42 = Utf8 Ljava/io/PrintStream;
#43 = Utf8 java/io/PrintStream
#44 = Utf8 println
#45 = Utf8 (Ljava/lang/String;)V
{
java.lang.Object o;
descriptor: Ljava/lang/Object;
flags:

public Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field o:Ljava/lang/Object;
15: return
LineNumberTable:
line 1: 0
line 2: 4
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this LMain;

public void m1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field o:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #5 // String ------hello synchronized m1
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any
LineNumberTable:
line 5: 0
line 6: 7
line 7: 15
line 8: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 this LMain;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 20
locals = [ class Main, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4

public synchronized void m2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String ------hello synchronized m2
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LMain;

public static synchronized void m3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String ------hello synchronized m3
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 15: 0
line 16: 8
}
SourceFile: "Main.java"

总结:

  • synchronized同步代码块,实现使用的是moniterentermoniterexit指令
    • 如果同步代码块中出现Exception或者Error,则会调用第二个monitorexit指令来保证释放锁。
    • 如果主动throw一个RuntimeException,一个enter对应一个exit
  • synchronized普通同步方法,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor。
  • synchronized静态同步方法,ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

管程

管程:Monitor(监视器),也就是我们平时说的锁,监视器锁。

管程是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中(把信号量及其操作原语“封装”在一个对象内部)。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。

ObjectMonitor

为什么任何一个对象都可以成为一个锁?

  • Java Object 类是所有类的父类,子类可以使用 Object 的所有方法。而Object的wait,nofity方法的底层支持实现是ObjectMonitor,它也实现锁控制。所以每个对象天生都带着一个对象监视器ObjectMonitor。

  • 在HotSpot虚拟机中,monitor采用ObjectMonitor实现:ObjectMonitor.javaObjectMonitor.cppobjectMonitor.hpp


ObjectMonitor.cpp 中引入了头文件(include)objectMonitor.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
140
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0;//锁的重入次数
_object = NULL;
_owner = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我
_WaitSet = NULL; //存放处于wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//存放处于等待锁block状态的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}

公平锁和非公平锁

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)。

公平锁是指多个线程按照申请锁的顺序来获取锁。

按序排队公平锁,就是判断同步队列是否还有先驱节点的存在,如果没有先驱节点才能获取锁;
先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以


ReentrantLock抢票案例:

ReentrantLock默认是非公平锁,运行后可以看到每个线程买票的数量不均匀,甚至有的线程没有卖到票;

传入true参数则是公平锁,卖票一开始是a占优,后面像a b c a b c a b c这样均匀分布。

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
class Ticket {
private int number = 30;
ReentrantLock lock = new ReentrantLock();
//ReentrantLock lock = new ReentrantLock(true);

public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

public class Main {
public static void main(String[] args) {
Ticket ticket = new Ticket();

new Thread(() -> {
for (int i = 0; i < 35; i++) ticket.sale();
}, "a").start();
new Thread(() -> {
for (int i = 0; i < 35; i++) ticket.sale();
}, "b").start();
new Thread(() -> {
for (int i = 0; i < 35; i++) ticket.sale();
}, "c").start();
}
}

为什么会有公平锁/非公平锁的设计?为什么默认是非公平?什么时候用公平和用非公平?

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间

  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

  3. 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。

所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

隐式锁Synchronized

隐式锁(即synchronized关键字使用的锁)默认是可重入锁。

同步代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main
{
public static void main(String[] args)
{
final Object objectLockA = new Object();

new Thread(() -> {
synchronized (objectLockA)
{
System.out.println("-----外层调用");
synchronized (objectLockA)
{
System.out.println("-----中层调用");
synchronized (objectLockA)
{
System.out.println("-----内层调用");
}
}
}
},"a").start();
}
}

同步方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ReEntryLockDemo
{
public synchronized void m1()
{
System.out.println("-----m1");
m2();
}
public synchronized void m2()
{
System.out.println("-----m2");
m3();
}
public synchronized void m3()
{
System.out.println("-----m3");
}

public static void main(String[] args)
{
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();

reEntryLockDemo.m1();
}
}

Synchronized的重入的实现机理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
140
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0;//锁的重入次数
_object = NULL;
_owner = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我
_WaitSet = NULL; //存放处于wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//存放处于等待锁block状态的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

首次加锁:当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

重入:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

释放锁:当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

显式锁ReentrantLock

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
public class ReEntryLockDemo
{
static Lock lock = new ReentrantLock();

public static void main(String[] args)
{
new Thread(() -> {
lock.lock();
try
{
System.out.println("----外层调用lock");
lock.lock();
try
{
System.out.println("----内层调用lock");
}finally {
// 这里故意注释,实现加锁次数和释放次数不一样
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock(); // 正常情况,加锁几次就要解锁几次
}
}finally {
lock.unlock();
}
},"a").start();

new Thread(() -> {
lock.lock();
try
{
System.out.println("b thread----外层调用lock");
}finally {
lock.unlock();
}
},"b").start();
}
}

死锁及排查

定义

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

死锁产生的原因:

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

死锁代码case

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
import java.util.concurrent.TimeUnit;

public class Main {
public static void main(String[] args) {
final Object objectLockA = new Object();
final Object objectLockB = new Object();

new Thread(() -> {
synchronized (objectLockA) {
System.out.println(Thread.currentThread().getName() + "\t" + "自己持有A,希望获得B");
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLockB) {
System.out.println(Thread.currentThread().getName() + "\t" + "A-------已经获得B");
}
}
}, "A").start();

new Thread(() -> {
synchronized (objectLockB) {
System.out.println(Thread.currentThread().getName() + "\t" + "自己持有B,希望获得A");
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLockA) {
System.out.println(Thread.currentThread().getName() + "\t" + "B-------已经获得A");
}
}
}, "B").start();

}
}

排查

纯命令:

  • jps -l 查看当前进程运行状况
  • jstack 进程编号 查看该进程信息
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
PS D:\javaProjects\JUC\src> jps -l
10280 sun.tools.jps.Jps
14040 Main
10204 org.jetbrains.jps.cmdline.Launcher
9868
PS D:\javaProjects\JUC\src> jstack 14040
2022-10-07 00:37:37
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.311-b11 mixed mode):
......
Found one Java-level deadlock:
=============================
"B":
waiting to lock monitor 0x000001cd1a7b6c38 (object 0x0000000780cc9668, a java.
lang.Object),
which is held by "A"
"A":
waiting to lock monitor 0x000001cd1a7b9578 (object 0x0000000780cc9678, a java.
lang.Object),
which is held by "B"

Java stack information for the threads listed above:
===================================================
"B":
at Main.lambda$main$1(Main.java:33)
- waiting to lock <0x0000000780cc9668> (a java.lang.Object)
- locked <0x0000000780cc9678> (a java.lang.Object)
at Main$$Lambda$2/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"A":
at Main.lambda$main$0(Main.java:18)
- waiting to lock <0x0000000780cc9678> (a java.lang.Object)
- locked <0x0000000780cc9668> (a java.lang.Object)
at Main$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

图形化:

  • win + r 输入jconsole ,打开图形化工具,打开线程 ,点击 检测死锁

小结

指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp,C++实现的)