说说Java锁事

3、说说Java锁事

3.1 大厂面试题复盘

3.2 悲观锁

  • 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
  • synchronized关键字和Lock的实现类都是悲观锁。

3.3 乐观锁

  • 乐观锁
    • 认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
    • 在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
    • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
    • 如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等。
  • 判断规则
    • 版本号机制Version。
    • 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

3.4 伪代码说明

3.5 锁相关的8种案例演示code

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
class Phone //资源类
{
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----sendEmail");
}

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

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

/**
* 题目:谈谈你对多线程锁的理解,8锁案例说明
* 口诀:线程 操作 资源类
* 8锁案例说明:
* 1 标准访问有ab两个线程,请问先打印邮件还是短信 --> 先邮件再短信
* 2 sendEmail方法中加入暂停3秒钟,请问先打印邮件还是短信 --> 先邮件再短信
* 3 添加一个普通的hello方法,请问先打印邮件还是hello --> 先hello再邮件
* 4 有两部手机,请问先打印邮件还是短信 --> 先短信再邮件
* 5 有两个静态同步方法,有1部手机,请问先打印邮件还是短信 --> 先邮件再短信
* 6 有两个静态同步方法,有2部手机,请问先打印邮件还是短信 --> 先邮件再短信
* 7 有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信 --> 先短信再邮件
* 8 有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信 --> 先短信再邮件
* <p>
* 笔记总结:
* 1-2
* * * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
* <p>
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
* <p>
* * 7-8
* * 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
* * *
* * * 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
* * * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* * *
* * * 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
* * * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
*/
public class Lock8Demo {
public static void main(String[] args)//一切程序的入口
{
Phone phone = new Phone();
Phone phone2 = new Phone();

new Thread(() -> {
phone.sendEmail();
}, "a").start();

//暂停毫秒,保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(() -> {
//phone.sendSMS();
//phone.hello();
phone2.sendSMS();
}, "b").start();
}
}

3.6 解释说明-小总结

3.7 JDK源码(notify方法)说明举例

3.8 synchronized同步代码块的反编译

3.9 m1方法里面自己添加一个异常试试

3.10 synchronized普通同步方法的反编译

3.11 大厂面试题讲解

3.12 管程

3.13 objectMonitor.hpp

3.14 提前剧透,混个眼熟

  • synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于MarkWord中的锁标志位和释放偏向锁标志位,后续讲解锁升级时候我们再加深,目前为了承前启后的学习,对下图先混个眼熟即可,O(∩_∩)O。

3.15 从ReentrantLock卖票demo演示公平和非公平现象

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 //资源类,模拟3个售票员卖完50张票
{
private int number = 50;

ReentrantLock lock = new ReentrantLock(true);

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

public class SaleTicketDemo {
public static void main(String[] args)//一切程序的入口
{
Ticket ticket = new Ticket();

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

3.16 何为公平锁/非公平锁?

3.17 为什么会有公平锁/非公平锁的设计?为什么默认非公平?

  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

3.18 什么时候用公平?什么时候用非公平?

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

3.19 预埋伏AQS

3.20 可重入锁(又名递归锁)的说明

  • 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
  • 如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

3.21 同步块

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

new Thread(() -> {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ----外层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ----中层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ----内层调用");
}
}
}
}, "t1").start();
}
}

3.22 同步方法

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
public class Main {
public synchronized void m1() {
//指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
System.out.println(Thread.currentThread().getName() + "\t ----come in");
m2();
System.out.println(Thread.currentThread().getName() + "\t ----end m1");
}

public synchronized void m2() {
System.out.println(Thread.currentThread().getName() + "\t ----come in");
m3();
}

public synchronized void m3() {
System.out.println(Thread.currentThread().getName() + "\t ----come in");
}

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

new Thread(() -> {
main.m1();
},"t1").start();
}
}

3.23 objectMonitor.hpp

3.24 显式锁(即Lock)也有ReentrantLock这样的可重入锁

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

public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t ----come in外层调用");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t ----come in内层调用");
} finally {
lock.unlock();
}

} finally {
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock();// 正常情况,加锁几次就要解锁几次
}
}, "t1").start();
}
}

3.25 请写一个死锁代码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
public class DeadLockDemo {
public static void main(String[] args) {
final Object objectA = new Object();
final Object objectB = new Object();

new Thread(() -> {
synchronized (objectA) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有A锁,希望获得B锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectB) {
System.out.println(Thread.currentThread().getName() + "\t 成功获得B锁");
}
}
}, "A").start();

new Thread(() -> {
synchronized (objectB) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有B锁,希望获得A锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectA) {
System.out.println(Thread.currentThread().getName() + "\t 成功获得A锁");
}
}
}, "B").start();
}
}

3.26 小总结(重要)

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