线程安全问题

两个基本概念

临界区

  • 多个线程读取共享资源的时候不会出现问题
  • 但是多个线程对共享资源进行写操作的时候可能会出现指令的交错的问题
  • 如果一段代码内存在对于共享资源的多线程读写操作,那么我们称这段代码为临界区

竞态条件

多个线程在临界区内执行,由于代码的执行序列不同导致结果无法预测,称之为发生了竞态条件

synchronized

对象锁,他采用互斥的方式让同一时刻最多只有一个线程能够持有对象锁,其他线程想要读写临界区中的资源,就会进入阻塞状态,不用担心因为线程上下文切换而引发的指令交错问题。

1
2
3
synchronized(对象) {
// 临界区
}

img

原理:用 对象锁 确保了临界区内代码的原子性

也可以在方法上加上 synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test{
public synchronized void test() {

}
}
等价于
class Test{
public void test() {
synchronized(this) {

}
}
}

注意:

如果加的锁的对象是一致的话,才能够实现原子性,不同锁的对象相当于不加锁,例如:

1
2
3
4
5
6
7
synchronized(A.class) {
// 临界区
}

synchronized(B.class) {
// 临界区
}

无法实现线程安全,因为他们对于锁的对象的限制是不同的。

同样的,对于 static 方法,也需要注意,因为他们如果加在方法上,说明他们锁的是类对象,而不是实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Number{
public static synchronized void a() {
// 临界区
}
}

class main{
public static void main(String[] args) {
Number num1 = new Number();
new Thread(() -> {
synchronized(num1) {
// 临界区
}
});
new Thread(() -> {
Number.a();
})
}
}

这段代码也没有实现线程安全问题的解决,因为他们加锁的对象不是同一个,其中一个是实例对象类型,一个是 class 类型

变量的线程安全问题

成员变量和局部变量是否线程安全

  • 如果他们没有共享,则他们是线程安全的。

  • 如果他们被共享了:

    • 如果只有只读的操作,则他们线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全问题。

局部变量是否线程安全

  • 局部变量是线程安全的

  • 但局部变量引用的对象不一定

    • 如果该对象没有逃离方法的作用访问,那么他是线程安全的
    • 如果该对象逃离方法的作用范围,那么需要考虑线程安全问题(可能同时被其他类引用)

常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • HashTable
  • java.util.concurrent 包下的类

他们的方法都是原子的,因为涉及到操作系统底层的内容

Monitor

mark Word对象标记,存储对于该对象的加锁情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|

Monitor 监视器模型

img

上图是Monitor监视器模型,他有WaitSet, EntrySet, Owner三个模型

当调用的临界区加上了 synchronized 对象锁后,就会触发属于该对象的监视器,注意,他和 synchronized 一样,每个对象的监视器是独有的。

  • Thread1 调用了带有 synchronized 的临界区后,他会进入到 monitor 对象,检查其中的 Owner 是否有线程占用,没有的话则自己占用。
  • Thread2,3,4调用临界区后,发现 Owner 已经被 Thread1 占有了,那么他们会进入到 EntrySet 中进行等待,并且进入 Block 阻塞状态。
  • Thread0 因为某些原因在运用时候被打断了(比如被别的线程调用wait()指令),那么他就会进入到 WaitSet 区域等待唤醒,并且进入 Waiting 状态。

Synchronized

Synchronized 原理

字节码:

1
2
3
4
5
6
7
8
9
10
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

我们可以看到,他实质上是运用了monitorentermonitorexit进行对象的监控,也就是说,Synchronized 是基于 Monitor 使用的。

轻量级锁

如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

对于使用者,仍是调用Synchronized,但底层已经优化

Synchronized 加锁的过程

  • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

img

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

img

  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

img

  • 如果 cas 失败,有两种情况

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

img

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

img

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

开始我们检测到 synchronized 时候,加上的是轻量级锁,那么当我们要用 cas 操作加上轻量级锁的时候发现操作失败,说明有一种情况是已经有轻量级锁的存在了,那么这时候需要将锁升级为重量级锁。这个过程就叫做锁膨胀

img

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED

img

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。即多试几次。

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

开启偏向锁后,对象的 markword 值最后三位是 101

没有开启偏向锁,对象创建后 markword 的值为 001

撤销偏向锁

  • 调用对象 hashCode
  • 其它线程使用对象
  • 调用 wait/notify

批量重偏向

当撤销偏向锁阈值超过 20 次后,jvm 给这些对象加锁时重新偏向至加锁线程

批量撤销

当撤销偏向锁阈值超过 40 次后,整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁消除

当只有一个临界区加上锁,且只有一个线程执行,那么系统会自动运用锁消除技术。减轻系统负担。

wait notify notifyAll

sleep() 和 wait() 的区别

  1. sleep() 是 Thread 类的方法,wait() 是 object 类的方法
  2. wait() 一定要在 synchronized 代码块中使用,而 sleep() 不一定
  3. sleep() 不会释放对象锁,wait() 会释放对象锁
  4. 它们的状态都是 TIMED_WAITING

notify 随机唤醒线程,并不固定,可以采用 notifyAll 进行全部唤醒

同步设计模式——保护式暂停

guardObject

两个线程同时监听着一个中间件,生产者将产品置为成员变量,消费者监听到成员变量里的值不为null,才取出产品。

img

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
@Slf4j(topic = "c.GuardedSuspension")
public class GuardedSuspension {

public static void main(String[] args) {
GuardObject go = new GuardObject();
// 线程1 等待 线程2 传递的资源
new Thread(() -> {
log.debug("开始等待获取结果");
Object o = go.get();
log.debug("获取到了结果: {}", o);
}, "t1").start();

new Thread(() -> {
try {
log.debug("start");
Thread.sleep(2000);
go.complete("result");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t2").start();
}

}

@Slf4j
class GuardObject {
/**
* 结果储存
*/
private Object response;

/**
* 锁
*/
private final Object lock = new Object();

public Object get() {
synchronized(lock) {
// 没有值的情况下,一直等待
while(response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("获取到结果:{}", response);
return response;
}
}

public void complete(Object response) {
synchronized (lock) {
this.response = response;
lock.notifyAll();
}
}
}

带超时版

这个的设计主要就是 join()方法的设计,其主要运用了当前时间和运行时间的比较,再将他们进行对比。

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
@Slf4j(topic = "c.GuardedSuspensionPassedTime")
public class GuardedSuspensionPassedTime {

public static void main(String[] args) {
GuardObjectPassedTime go = new GuardObjectPassedTime();
// 线程1 等待 线程2 传递的资源
new Thread(() -> {
log.debug("开始等待获取结果");
Object o = go.get(3000L);
log.debug("获取到了结果: {}", o);
}, "t1").start();

new Thread(() -> {
try {
log.debug("start");
Thread.sleep(2000);
go.complete("result");
log.debug("获取结果成功");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t2").start();
}

}

@Slf4j(topic = "c.GuardObjectPassedTime")
class GuardObjectPassedTime {
/**
* 结果储存
*/
private Object response;

/**
* 锁
*/
private final Object lock = new Object();

public Object get(long millis) {
long current = System.currentTimeMillis();
long passed = 0L;
synchronized(lock) {
// 没有值的情况下,一直等待
while(response == null) {
if(passed >= millis) {
break;
}
try {
lock.wait(millis - passed);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 已经等待的时间
passed = System.currentTimeMillis() - current;
}
return response;
}
}

public void complete(Object response) {
synchronized (lock) {
this.response = response;
lock.notifyAll();
}
}
}

面向对象demo

主要运用了一个 Futures中间件,将对象们封装了起来

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
@Slf4j(topic = "c.GuardedSuspensionMutiple")
public class GuardedSuspensionMutiple {

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Thread.sleep(1000);
for(Integer id : Futures.getKeySet()) {
new Postman(id, "message" + id).start();
}
}

}


@Slf4j(topic = "c.People")
class People extends Thread {
@Override
public void run() {
log.debug("开始等待收信");
GuardMutipleObject guardMutipleObject = Futures.createGuardMutipleObject();
Object o = guardMutipleObject.get();
log.debug("收到信了: id: {} response: {}",guardMutipleObject.getId(), o);
}
}

@Slf4j(topic = "c.Postman")
class Postman extends Thread {
private Integer postmanId;

private String message;

public Postman(Integer postmanId, String message) {
this.postmanId = postmanId;
this.message = message;
}


public Integer getPostmanId() {
return postmanId;
}

public String getMessage() {
return message;
}

@Override
public void run() {
GuardMutipleObject guardMutipleObject = Futures.getGuardMutipleObject(postmanId);
log.debug("送信 id:{}, 内容:{}", postmanId, message);
guardMutipleObject.complete(message);
}
}

class Futures {

private static Map<Integer, GuardMutipleObject> guardMutipleObjectMap = new Hashtable<>();

private static Integer id = 0;

private static synchronized Integer generateId() {
return id++;
}

/**
* 获取任务并且移除
*/
public static GuardMutipleObject getGuardMutipleObject(Integer id) {
return guardMutipleObjectMap.remove(id);
}

/**
* 创建任务
*/
public static GuardMutipleObject createGuardMutipleObject() {
GuardMutipleObject guardMutipleObject = new GuardMutipleObject(generateId());
guardMutipleObjectMap.put(guardMutipleObject.getId(), guardMutipleObject);
return guardMutipleObject;
}

/**
* 获取任务编号
*/
public static Set<Integer> getKeySet() {
return guardMutipleObjectMap.keySet();
}
}

@Slf4j
class GuardMutipleObject {

public Integer getId() {
return id;
}

private Integer id;

public GuardMutipleObject(Integer id) {
this.id = id;
}

/**
* 结果储存
*/
private Object response;

/**
* 锁
*/
private final Object lock = new Object();

public Object get() {
synchronized(lock) {
// 没有值的情况下,一直等待
while(response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("获取到结果:{}", response);
return response;
}
}

public void complete(Object response) {
synchronized (lock) {
this.response = response;
lock.notifyAll();
}
}
}

异步设计模式——消费者和生产者

类比消息队列,但是消息队列中是进程之间的关系,而这里是线程之间的关系

img

这里主要相比较于前面的同步,无需再做到对象的一一对应,生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据。且他是有容量限制的,满时不会再加入数据,空时不会再消耗数据

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
package com.arong.JUC.async;

import lombok.extern.slf4j.Slf4j;

import java.util.Deque;
import java.util.LinkedList;

@Slf4j(topic = "c.demo")
public class demo {


public static void main(String[] args) {

MessageQueue mq = new MessageQueue(2);

new Thread(() -> {
while (true) {
StandardMessage message = mq.take();
String response = message.getMessage();
log.debug("take message({}): [{}] lines", message.getId(), response);
}
}, "消费者").start();

for (int i = 0; i < 4; i++) {
StandardMessage standardMessage = new StandardMessage(i, "往消息队列发送了消息:" + i);
new Thread(()-> {
log.debug("发送消息: {}", standardMessage);
mq.put(standardMessage);
},"生产者" + i).start();
}

}
}

@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
/**
* 容量
*/
private final Integer Capacity;

/**
* 存储信息的队列
*/
private final Deque<StandardMessage> list = new LinkedList<>();

MessageQueue(Integer capacity) {
this.Capacity = capacity;
}

public void put(StandardMessage sm) {
synchronized (list) {
// 队列满了的时候
while(list.size() == Capacity) {
log.debug("队列已满");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.addFirst(sm);
list.notifyAll();
}
}

public StandardMessage take() {
synchronized (list) {
while(list.isEmpty()) {
log.debug("队列已空");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
StandardMessage standardMessage = list.removeLast();
list.notifyAll();
return standardMessage;
}
}

}


class StandardMessage {
private Integer id;

private String message;

public Integer getId() {
return id;
}

public String getMessage() {
return message;
}

@Override
public String toString() {
return "StandardMessage{" +
"id=" + id +
", message='" + message + '\'' +
'}';
}

public StandardMessage(Integer id, String message) {
this.id = id;
this.message = message;
}
}

Park & UnPark

与 Object 的 wait & notify 相比 :

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

线程状态

假设有线程 Thread t

情况 1 NEW –> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <–> WAITING

t 线程synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程RUNNABLE --> WAITING

  • 调用 obj.notify()obj.notifyAll()t.interrupt()

    • 竞争锁成功,t 线程WAITING --> RUNNABLE
    • 竞争锁失败,t 线程WAITING --> BLOCKED

情况 3 RUNNABLE <–> WAITING

  • 当前线程调用 t.join() 方法时,当前线程RUNNABLE --> WAITING

注意是当前线程t 线程对象的监视器上等待

  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程WAITING --> RUNNABLE

情况 4 RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <–> TIMED_WAITING

  • 调用 obj.wait(long n) 方法时,t 线程RUNNABLE --> TIMED_WAITING

  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

    • 竞争锁成功,t 线程TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程RUNNABLE --> TIMED_WAITING

注意是当前线程t 线程对象的监视器上等待

  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING--> RUNNABLE

情况 9 RUNNABLE <–> BLOCKED

  • t线程用synchronized(obj) 获取了对象锁时如果竞争失败,从RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然BLOCKED

情况 10 RUNNABLE <–> TERMINATED

  • 当前线程所有代码运行完毕,进入 TERMINATED

java线程

创建和运行线程

直接使用 Thread 创建线程

1
2
3
4
5
6
7
8
9
// 新建线程对象 参数为线程名称
Thread t1 = new Thread("t1") {
@override
public void run() {
// 要执行的任务
}
};
// 启动线程
t1.start();

使用 Runnable 配合 Thread

runnable 代表了可运行的任务,将线程和任务进行分离

1
2
3
4
5
6
7
8
9
10
Runnable runnable = new Runnable() {
@override
public void run() {
// 要执行的任务
}
};
// 创建线程对象
Thread t2 = new Thread(runnbale, "t2");
// 启动线程
t2.start();

FutureTask 配合 Thread

FutureTask 也是表示了任务,他和 runnable 的区别在于他有返回值,而 Runnable 没有返回值。

1
2
3
4
5
6
7
8
9
10
 // 创建任务对象 泛型表示返回的对象
// lambda表达式将函数作为参数进行传递
FutureTask<Integer> task3 = new FutureTask<>(() -> {
return 100;
});

new Thread(task3, "t3").start();

// 获取返回结果
Integer result = task3.get();

多个线程同时运行

  • 交替运行
  • 先后顺序不由我们控制,而由底层的操作系统进行控制

查看进程线程的方法

windows

taskList 查看进程
taskkill 杀死进程

linux

ps -fe 查看所有进程
ps -fT -p <PID> 查看某个进程(PID)的所有线程
kill 杀死进程

java

jps 查看所有的 java 进程
jstack <PID> 查看某个 Java 进程的所有线程状态
jconsole 查看运行状态(图形界面)

线程运行的原理

栈与栈帧

  • 每个线程启动之后,虚拟机会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame) 组成,对应着每次方法调用时候所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

线程的上下文切换

上下文切换,即 CPU 分配的时间片由一个线程转为另一个线程,也即切换线程
导致上下文切换可能的原因:

  • 线程的 CPU 时间片用完
  • 垃圾回收
  • 有更高级的线程需要运行
  • 线程自己调用了 sleep,yield,wait,join,park,synchronized,lock 等方法
    当上下文切换执行后,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。java 中通常通过程序计数器来实现,上下文切换的频繁发生会影响性能。

线程的常见方法

run() 和 Start()

  • run() 方法是线程中一个重写执行逻辑的方法,如果在主程序单独的调用线程的run方法,只是实现了其中的逻辑,而没有启动线程。
  • start() 启动了新的线程,在新的线程中执行 run() 方法等方法。

sleep() 和 yield()

  • sleep()
    ○ 调用他会让线程从 Running 进入到 Timed Waiting 状态(阻塞)
    ○ 可以使用 interrupt 打断正在睡眠的线程,此时会抛出 InterruptedException错误。
  • yield()
    ○ 调用他会让线程从 Running 进入到 Runnable 状态
    ○ 具体执行逻辑取决于任务调度器

join()

等待 join() 的调用者的任务执行完成之后,在进行下一步的操作。可以理解成局部线程的同步。其中参数可以添加时间,取到线程执行任务时间和参数传递的时间的最小值作为最多等待的时间。

interrupt

打断 sleep, wait, join 的线程,并抛出InterruptedException异常,如果是 sleep 状态的线程被打断,会清空打断状态,即t.isInterrupted() = false
如果是打断正常运行的线程,那么t.isInterrupted() = true

主线程与守护线程

只要非守护线程运行结束了,那么即使守护线程的代码没有执行完成,也会强制结束。
设置守护线程:t1.setDaemon(true);

线程的状态

五种状态(从操作系统层面来讲)

  • [初始状态],刚 new 出来的对象,仅仅是对象层面,还没有和线程相关联。
  • [可运行状态],指该线程已经被创建,可以被 CPU 调度运行。
  • [运行状态],获取了CPU时间片的运行中的状态,当 CPU 时间片用完时,会从运行状态转换为可运行状态,导致线程的上下文切换。
  • [阻塞状态],线程进行上下文切换到其他的状态,只要一直不被唤醒,调度器就一直不会考虑调度他们。
  • [终止状态],表示线程已经执行完毕,生命周期已经结束,不会再转变为其他的状态。

六种状态(从 JAVA API 的层面描述)

  • new, 如同操作系统中的初始状态,指线程刚刚被创建
  • runnable, 包括了操作系统中的可运行状态,运行状态和阻塞状态
  • blocked,加锁导致的阻塞
  • waiting,调用 wait()方法之后的阻塞
  • timed_waiting, 调用 wait()方法之后的阻塞,但是有时限
  • terminated,终止状态。

线程与进程

1. 进程与线程

进程与线程

  • 进程:可被视为程序的一个实例,可以视为由指令和数据组成。进程就是用来加载指令,管理内存,管理io的。
  • 线程:一个进程之内可以有多个线程同时运行,线程可以视为是进程的一个子集。可被视为一个个指令流,将指令流中的一条条指令依次顺序执行。
    • 线程为最小调度单位,进程为资源分配的最小单位
    • 进程拥有共享的资源,如内存空间可以使内部的线程实现共享。

并行和并发

  • 并发是同一时间应对多件事情的能力
  • 并行是同一时间动手做多件事情的能力

同步和异步

  • 同步指的是需要等待结果返回才能继续运行
  • 异步是指无需等待结果返回,仍然能够运行
  1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活

  2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分,也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没有意义

  3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

观察者模式

1.定义

观察者模式定义了一种一堆多的依赖关系,当一个对象的状态发生改变时,所有依赖于它对象都会得到通知。观察者模式也被成为发布-订阅模式。实现了解耦,在需要新增一个观察者的时候只需要new一个出来,然后注册到主题对象就可以了,在观察者不需要观察该主题对象,只需要在主题对象的观察者列表中去除该观察者对象即可。

2.结构图

img

  • Subject: 被观察者的对象抽象类。其中包含了观察者的集合,添加和删除观察者的方法。还有通知观察者观察对象已改变的方法
  • Observer:抽象观察者类。只有一个响应接口,响应接口中定义了被观察者。响应接口除了需要做出需要做的事情之外,还需要将自己注册到被观察者中去。

3.实例

被观察者的对象抽象类

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 abstract class Subject {
/**
* 定义一个集合存储被观察者
*/
List<Observer> observerList = new ArrayList<>();

/**
* 添加监听者
* @param observer
*/
public void addObserver(Observer observer) {
observerList.add(observer);
}

/**
* 删除监听者
* @param observer
*/
public void deleteObserver(Observer observer) {
observerList.remove(observer);
}

// 做出什么事情后,监听者会有所反应
public abstract void signature();

public void report() {
for(Observer observer : observerList) {
observer.response(this);
}
}
}

观察者抽象类

1
2
3
4
public interface Observer {
// 监听者的反应
public void response(Subject subject);
}

具体被观察者类

1
2
3
4
5
6
7
public class Student extends Subject {
@Override
public void signature() {
System.out.println("我要吃饭呜呜呜");
report();
}
}

具体观察者类

1
2
3
4
5
6
7
public class Teacher implements Observer {
@Override
public void response(Subject subject) {
subject.addObserver(this);
System.out.println("老师说:好好学习,以后每顿都能吃上两个肉");
}
}

策略模式

1.定义

其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。
其主要目的是通过定义相似的算法,替换if else 语句写法,并且可以随时相互替换。

2.结构图

img

  • 环境角色(Context):持有一个策略类的引用,提供给客户端使用。
  • 抽象策略角色(Strategy):这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  • 具体策略角色(ConcreteStrategy):包装了相关的算法或行为。

3.实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class StrategyUseService implements ApplicationContextAware {
private Map<UpgradeWay, Upgrade> upgradeMap = new ConcurrentHashMap<>();

public Long deResolve(UpgradeWay upgradeWay, Long experience) {
upgradeWay = Optional.ofNullable(upgradeWay).orElse(UpgradeWay.SLEEP);
Upgrade upgrade = upgradeMap.get(upgradeWay);
if(upgrade == null) {
return null;
}
return upgrade.resolve(experience);
}


// spring初始化初始化时候即初始化各种策略
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Upgrade> beansOfType = applicationContext.getBeansOfType(Upgrade.class);
beansOfType.values().forEach(strategyService -> upgradeMap.put(strategyService.gainUpgradeWay(), strategyService));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Upgrade {
/**
* 获取策略类型
* @return 策略类型
*/
UpgradeWay gainUpgradeWay();

/**
* 具体实现逻辑
* @param experience 原有经验值
*/
Long resolve(Long experience);
}
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class SleepUpgrade implements Upgrade{
@Override
public UpgradeWay gainUpgradeWay() {
return UpgradeWay.SLEEP;
}

@Override
public Long resolve(Long experience) {
return this.gainUpgradeWay().getValue() + experience;
}
}

内存

缓存池的作用

缓存池主要是为了加快数据的查询效率。如果每个查询语句都到磁盘中的数据库进行查询的话,那么进行io执行效率不高。所以在内存中开辟了一块空间,用来存储高频的查询语句,目的是为了提高查询效率。

  • 当执行一条查询语句时,执行器会先到缓存池中寻找,如果命中,就直接返回。如果未命中,就到磁盘里读取数据,在将这条数据加入到缓存池中。
  • 当执行一条更新语句时,会先进行查询定位要原始语句,流程同上,更新完会同时更新缓存池中的数据,同时将缓存池中的数据标记为脏页。等到合适的时候将内存刷盘,更新到磁盘中。

缓存池的容量

Buffer Pool 是在 MySQL 启动的时候,向操作系统申请的一片连续的内存空间,默认配置下 Buffer Pool 只有 128MB

可以通过调整 innodb_buffer_pool_size 参数来设置 Buffer Pool 的大小,一般建议设置成可用物理内存的 60%~80%

缓存池的存储结构

在Inno db中,数据的存储以页为单位。一个页中有保存了多条数据。根据缓存一致性原理,查询的时候会将其附近的语句一起加载。那么在缓存池中,也会同样以页为单位进行存储,一个页的大小默认为16kb。每个页中的结构有数据页,索引页,插入缓存页,undo log页,自适应哈希索引页,锁信息页。为了能够更好的管理页,每个页开头都有一个控制块进行管理,控制块中的信息包括缓存页的表空间、页号、缓存页地址、链表节点等。

缓存池的功能

管理空闲页

img

采用链表结构,将控制块作为链表节点。通过设置一个free链表来快速定位到空闲页的位置。减少了查询的数据量。

管理脏页

img

基本上相当于空闲页的结构。

提高缓存命中率

原始LRU结构

LRU数据结构

LRU主要是为了提高数据读取效率而设计的。他的主要功能是最近访问的信息会出现在链表的头部,所以他被叫做:last receive user(最近使用)

最朴素的LRU结构的特点就是,

  • 查询数据的时候,如果数据在LRU链表中,就将其提前到链表的头部。
  • 如果数据不在链表中,就在内存中将数据插入LRU链表的头部,同时将LRU的最后一个元素退出队列。

但是这种结构很少被使用,因为他会导致一些问题:

  • 预读失效
  • 缓存池污染

解决预读失效

预读指的是因为内存的空间局部性原理,当查询到一个数据的时候也会将其临近内存地址的数据一起添加。为了减少磁盘io。针对于程序的空间局部性原理,是大部分情况下成立的,但是在一部分情况下预读的页完全没有被访问到。但是他会存储在链表头部,挤占后面链表的空间。大大降低了缓存的命中率。

解决预读失效的方法是将其划分为新生代和老年代,预读的页加入old区域的头部,等到真正读取到的时候,才进入young区域的头部。这样既兼顾了缓存一致性的原理,有防止因为预读失效而让热点数据过期的风险。等到新数据插入的时候,优先淘汰old区域的节点。young区域的最后一个节点进入old区域。

解决缓存池污染

当一条sql语句查询出大量数据的时候,他们可能只被查询了一次,但是却一起被加载到缓存区中。把原有的高频缓存数据挤占出去。

mysql在进入到 young 区域条件增加了一个停留在 old 区域的时间判断。

具体是这样做的,在对某个处在 old 区域的缓存页进行第一次访问时,就在它对应的控制块中记录下来这个访问时间:

  • 如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该缓存页就不会被从 old 区域移动到 young 区域的头部;
  • 如果后续的访问时间与第一次访问的时间不在某个时间间隔内,那么该缓存页移动到 young 区域的头部;

这个间隔时间是由 innodb_old_blocks_time 控制的,默认是 1000 ms。

也就说,只有同时满足「被访问」与「在 old 区域停留时间超过 1 秒」两个条件,才会被插入到 young 区域头部,这样就解决了 Buffer Pool 污染的问题 。

脏页刷入磁盘的时机

下面几种情况会触发脏页的刷新:

  • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
  • Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
  • MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
  • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;

日志

undo log

undo log的作用

img

  • 实现事务的回滚,保证事务的原子性

    • 其主要实现就是将当前数据库操作取反,写入undo log中,比如新增一条语句,就记录其主键,回滚时删除;删除语句则将对应数据存入日志中;修改则存入原语句。

很多人疑问 undo log 是如何刷盘(持久化到磁盘)的?

undo log 和数据页的刷盘策略是一样的,都需要通过 redo log 保证持久化。

buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。

  • 通过与Read View的配合实现MVCC多版本并发控制。一次事务操作产生的undo log中有trx_idroll_pointer两个参数。trx_id指的是事务的id,roll_pointer指的是一个指针,用来将事务联系起来。主要用来记录事务产生的先后顺序。这样结合read view可以实现可重复读和和一定程度上预防幻读。

    • undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。

img

Buffer pool缓存池的作用

其作用和内存的作用大差不差:

  • 当读取数据的时候,执行器会先有限在缓存池中找是否存在数据,如果命中缓存,则直接返回;如果没有命中,再到磁盘中进行寻找。
  • 当修改数据的时候,会先在缓存池中寻找要修改的数据是否存在,如果存在的话,会先修改缓存中的数据,同时将其标记为脏页。由参数指定将脏页由缓存更新到磁盘的时间。

在mysql启动的时候,Inno DB会在缓存池中建立一个一个的页,每页的大小为16kb,开始的时候页是空的,会随着一条条查询操作而填充页。

img

我们的undo log就记录在这当中的undo页中。

读取数据的时候,是一页一页的缓存,需要满足数据一致性原则。

redo log

redo log的作用

redo log是用于记录数据库事务的每一条修改,删除,新增操作。目的是为了防止数据库因为外部原因崩溃宕机而导致的数据丢失。redo log记录了一次事务完成后的数据状态,记录的是数据更新之后的值。

因为redo log是对于buffer pool中进行标记脏页(修改)的操作进行记录,所以undo log也会被记录到redo log中进行持久化。

在sql执行中redo log的功能:

img

undo log 和redo log在一个事务中的作用:

img

  • 有了 redo log,再通过 WAL 技术,InnoDB 就可以保证即使数据库发生异常重启,之前已提交的记录都不会丢失,这个能力称为crash-safe(崩溃恢复)。可以看出来, redo log 保证了事务四大特性中的持久性。
  • 将写入磁盘的方式由随机写改为顺序写,提高了io效率。

WAL技术:MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。采用了追加操作进行写入,所以WAL写入数据的方式是顺序写。

redo log刷盘时机分析

  • MySQL 正常关闭时;

  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;

  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。

  • 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制,下面会说)。

    • 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。
    • 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
    • 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache专门用来缓存文件数据的,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存。

InnoDB 的后台线程每隔 1 秒:

  • 针对参数 0 :会把缓存在 redo log buffer 中的 redo log ,通过调用 write() 写到操作系统的 Page Cache,然后调用 fsync() 持久化到磁盘。所以参数为 0 的策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失;
  • 针对参数 2 :调用 fsync,将缓存在操作系统中 Page Cache 里的 redo log 持久化到磁盘。所以参数为 2 的策略,较取值为 0 情况下更安全,因为 MySQL 进程的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。
  • 数据安全性:参数 1 > 参数 2 > 参数 0
  • 写入性能:参数 0 > 参数 2> 参数 1

binlog

redo log和binlog有什么区别

  • 适用对象的不同

    • redo log是inno db独有的日志格式
    • binlog是所有存储引擎都适用的,他服务于service层
  • 写入的方式不同

    • redo log采用的是循环写,会对前面的内容产生覆盖
    • binlog采用的是追加写,写满一个文件,会创建一个新的文件继续写,不会覆盖前面的内容
  • 作用不同

    • redo log主要为了防止突发的系统宕机导致数据不同步的问题
    • binlog主要是为了实现备份的恢复以及主从复制

如何实现主从复制

  • MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
  • 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
  • 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

关键点:写数据从主库操作,读数据从从库操作。异步执行,读写分离。但缺点是可能会信息无法立刻同步导致读取的字段是旧数据。

binlog的刷盘的时机

MySQL提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:

  • sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;
  • sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync;
  • sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。

sql语句更新的执行流程:

  1. 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
    • 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
    • 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
  1. 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
    • 如果一样的话就不进行后续更新流程;
    • 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
  1. 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
  2. InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。
  3. 至此,一条记录更新完了。
  4. 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。

两阶段提交

  • 如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。MySQL 重启后,通过 redo log 能将 Buffer Pool 中 id = 1 这行数据的 name 字段恢复到新值 xiaolin,但是 binlog 里面没有记录这条更新语句,在主从架构中,binlog 会被复制到从库,由于 binlog 丢失了这条更新语句,从库的这一行 name 字段是旧值 jay,与主库的值不一致性;
  • 如果在将 binlog 刷入到磁盘之后, MySQL 突然宕机了,而 redo log 还没有来得及写入。由于 redo log 还没写,崩溃恢复以后这个事务无效,所以 id = 1 这行数据的 name 字段还是旧值 jay,而 binlog 里面记录了这条更新语句,在主从架构中,binlog 会被复制到从库,从库执行了这条更新语句,那么这一行 name 字段是新值 xiaolin,与主库的值不一致性;

事务的提交过程有两个阶段,就是将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,具体如下:

  • prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);
  • commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;

类结构和类加载

1. 类文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}

1.1 魔数(amgic)

唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。即进行类型识别。使用魔数而不使用文件拓展名是为了安全考虑。

1
u4             magic; //Class 文件的标志

1.2 版本号(minor_version & major_version)

高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。

1
2
u2             minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号

1.3 常量池

1
2
u2             constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池

constant_pool_count表示常量池的容量,索引从1开始,到他的数量-1.第0项常量表示不引用任何常量,默认为空。

0x0016:表示为十进制是22,表示有21个常量,索引从1-21
主要存放两类常量:字面量和符号引用
每一项常量都是一个表,表开始是u1类型的标志位。

1.4 访问标志

识别类或接口层次的访问消息。

1
u2             access_flags;//Class 的访问标记

1.5 类索引,父类索引和接口索引集合

当前类要设置全限名,所有的都有父类,除了java.lang.Object,接口可以多实现。

1
2
3
4
u2             this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口

1.6 字段表集合

1
2
u2             fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有多个字段

字段表格式:

  • access_flags: 字段的作用域(public ,private,protected修饰符),是实例变量还是类变量(static修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。
  • name_index: 对常量池的引用,表示的字段的名称;
  • descriptor_index: 对常量池的引用,表示字段和方法的描述符;
  • attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;
  • attributes[attributes_count]: 存放具体属性具体内容。
    字段表标志位:

1.7 方法表集合

1
2
u2             methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法

方法表结构:

方法表标志位:

2. 类加载机制

类的生命周期:

类加载的时机:

  • 创建类的实例,也就是new一个对象。
  • 访问类的静态方法或者静态变量(包含静态变量赋值)。
  • 使用Class.forName()反射类。
  • 子类初始化的时候。
  • JVM启动时标明的启动类。

2.1 加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个流的静态存储结构转换为方法区运行时数据结构。
  3. 生成一个Class对象
    jvm是懒加载,所以只有使用到类时才会加载,例如调用类的main()方法,new对象等等 ,主类在运行过程中如果使用到其它类,会逐步加载这些类。

2.2 验证

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

2.3 准备

为类中定义的变量(被static修饰过的变量)分配内存并设置类变量初始值。此阶段不包含实例变量的赋值。

2.4 解析

将符号引用转换为直接引用。

  • 符号引用: 描述对象,包括如下三种:
    类和接口的全限定名
    字段的名称和描述符
    方法的名称和描述符
  • 直接引用:
    变量有一个内存地址来标识,如果我们用一个指针指向这个内存地址,这个指针就是直接引用。
    等我们需要用到这个变量的时候,就可以直接通过指针指向的地址找到。
    而我们在加载类的时候,解析代码并指向内存某个地址,然后将符号引用 obj和这个内存地址进行映射的过程,就是解析这个步骤要做的事,也叫做符号引用转换为直接引用。

2.5 初始化

(1)对类的静态变量初始化为指定的值
int initData = 666
(2)执行静态代码块
<clinit>()

3. 类加载器

3.1 类加载器的种类

  • 启动类加载器(Bootstrap ClassLoader):负责加载Java类的核心类(<JAVA_HOME>\lib目录下,能被-Xbootclasspath参数所指定路径存放的),是用C++代码实现的,无法被java代码直接引用。
  • 扩展类加载器(Extensions ClassLoader):负责加载JRE的扩展目录lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为Null。
  • 应用程序类加载器(Application ClassLoader): 负责加载用户类路径 classpath 上所有的 jar 包和 .class 文件。

3.2 双亲委派模型

工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的夹杂请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
简而言之:自下而上请求,自上而下加载
优势:
父类加载器成功加载则返回,子类加载器不会再加载,防止了重复加载。
防止核心API库被随意篡改。比如有一个要加载java.lang.Integer类的请求,通过双亲委派进制加载传递到启动类加载器,在在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,可以防止核心API被随意篡改。

责任链模式

1.基本概念

这是一个可以说是过关斩将的设计模式,责任链就像是一道道关卡,你只有闯过了上一关,才能到达下一关继续闯荡。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。

2.责任链模式的使用场景

  • 多条件流程判断:权限控制(此处指的是嵌套的循环判断)
  • ERP 系统流程审批:总经理、人事经理、项目经理
  • Java 过滤器的底层实现 Filter

3.结构图

img

  • AbstractHandler抽象责任人类:提供一个抽象类模板供下属责任人实现。有两个部分:其一是连接下一个责任人的对象以及其getter,setter方法;以及具体责任人要做的事情
  • Son具体责任人类:实现抽象责任人类,并后续会连接起来。

4.具体实现

抽象责任人类

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class AbstractHandler {
private AbstractHandler nextAbstractHandler;

public void setNextAbstractHandler(AbstractHandler nextAbstractHandler) {
this.nextAbstractHandler = nextAbstractHandler;
}

public AbstractHandler getNextAbstractHandler() {
return this.nextAbstractHandler;
}

public abstract String checkScore(int score);
}

具体责任人类

1
2
3
4
5
6
7
8
9
10
11
@Component
@Order(0)
public class FirstPass extends AbstractHandler {
@Override
public String checkScore(int score) {
if(score > 60 && this.getNextAbstractHandler() != null) {
return this.getNextAbstractHandler().checkScore(score);
}
return "很遗憾,没有通过第一关";
}
}

记得加上@Component注解之后才能在spring初始化的时候被注入,@Order(0)表示位于该链条的顺序。

初始化类

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
@Component
public class ChainService {

@Autowired
private List<AbstractHandler> abstractHandlerList;

private AbstractHandler abstractHandler;

private static final List<String> SCORE = new ArrayList<>();

@PostConstruct
public void initAbstractHandler() {
for (int i = 0; i < abstractHandlerList.size(); i++) {
if(i == 0) {
abstractHandler = abstractHandlerList.get(0);
} else {
AbstractHandler current = abstractHandlerList.get(i - 1);
AbstractHandler next = abstractHandlerList.get(i);
current.setNextAbstractHandler(next);
}
}
}

public List<String> getScore(int score) {
SCORE.add(abstractHandler.checkScore(score));
return SCORE;
}
}

责任链初始化

首先在spring初始化前准备一个存储责任链模板元素的list集合用来存储责任链元素(列表会自动装配其类型的bean元素)

@PostConstruct注解,将零散的责任链链条组装,成为一个完整的责任链。

还可以用枚举值的方式进行注入

全局锁

执行全局锁的命令:

1
flush tables with read lock

执行命令以后,整个表都会处于只读阶段。只读阶段阻塞了如下操作:

  • 对于数据的增删改(insert, update, delete)
  • 对于表结构的更改(drop,alter)

主要应用场景为全局逻辑备份

但是因为inno DB中有运用read view实现可重复读,修改后由于事务,数据库数据的一致性可以得到保证,所以造成无法改动的全局锁由于细粒度过大基本上被废弃。

但是,对于 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法。

表级锁

表锁

1
2
3
4
5
6
7
8
//表级别的共享锁,也就是读锁;
lock tables t_student read;

//表级别的独占锁,也就是写锁;
lock tables t_stuent write;

//释放当前会话的所有表锁
unlock tables

元数据锁(MDL)

元数据锁的目的是为了防止线程对表数据进行修改的时候,对于表格式进行变更,这个锁的对象是表的数据结构。

它分为读锁和写锁两种锁:

  • 当对于一张表做CRUD操作的时候,他新增的是MDL读锁
  • 当对于一张表做出表结构修改的时候,他新增的是MDL写锁

MDL锁的机制是读读共享,读写互斥,写写互斥。

申请MDL锁的事务会生成一个队列,写锁的优先级比读锁的要高。也就是说,当一张表加上了MDL读锁,之后有一个事务想要对表结构做出修改,加上MDL写锁,那么他之后的对于想要加入CRUD操作的读锁都是位于阻塞的状态。

意向锁

意向锁的目的是为了快速判断表中是否有记录被加上了行级锁。

原因是如果没有表的锁,需要一行一行判断有无加上行级锁,效率低下,加上意向锁可以达到剪枝的效果,排除完全没有被加上行级锁的表。

  • 当用InnoDB存储引擎的时候,当对于一条记录加上了共享锁,那么会对于其表加上意向共享锁。
  • 当用InnoDB存储引擎的时候,当对于一条记录加上了独占锁,那么会对于其表加上意向独占锁。

共享锁和独占锁是行级锁,意向独占锁和意向共享锁是表级锁。两者互不冲突。

AUTO-INC锁

在插入一条数据的时候,可以不输入主键就实现主键的自增,主要通过的就是auto-inc锁。

AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放

但是这种情况在大量插入的情况下,插入语句就变成了并行操作,容易导致执行效率的低下。

因此, 在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。

一样也是在插入数据的时候,会为被 AUTO_INCREMENT 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁

这个时候减少了锁存在的时间,由新增语句完成后才释放锁提前到主键赋值完成就释放锁,减少了锁存在的时间,提高了锁的性能。

行级锁

1
2
3
4
5
//对读取的记录加共享锁
select ... lock in share mode;

//对读取的记录加独占锁
select ... for update;

共享锁(S)满足读读共享,读写互斥,也就是可以被多个线程所持有。

独占锁(X)满足读写互斥,写写互斥,只能被一个线程所持有。

Record Lock

记录锁加锁的对象是一条数据。当对于一个事务加上了记录锁的时候,其他事务就不能对其进行修改。但是记录锁也有共享锁和独占锁的区别:

  • 当一个事务对一条记录加上了S(共享锁)的时候,其他事务仍然可以对其加上S锁,但不能对其加上X锁;
  • 当一个事务对一条记录加上了X(独占锁)的时候,其他事务不能对加上S锁,也不能加上X锁。

当提交时,所有的锁都会被释放。

Gap Lock

Gap Lock被称为间隙锁,只存在于可重复度级别,主要用于解决可重复度中的幻读问题。

比如表中有一个(2,5)的间隙锁,那么id = 4的这条记录就无法再次被插入了。

间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系。

Next-Key Lock

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的

行级锁不同情况下是如何加锁的

唯一索引等值查询

  • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」

img

  • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」

唯一索引非等值查询

当唯一索引进行范围查询时,会对每一个扫描到的索引加 next-key 锁,然后如果遇到下面这些情况,会退化成记录锁或者间隙锁

  • 情况一:针对「大于等于」的范围查询,因为存在等值查询的条件,那么如果等值查询的记录是存在于表中,那么该记录的索引中的 next-key 锁会退化成记录锁

  • 情况二:针对「小于或者小于等于」的范围查询,要看条件值的记录是否存在于表中:

    • 当条件值的记录不在表中,那么不管是「小于」还是「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。
    • 当条件值的记录在表中,如果是「小于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁;如果「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引 next-key 锁不会退化成间隙锁。其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。

非唯一索引等值查询

  • 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁
  • 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁

非唯一索引非等值查询

非唯一索引和主键索引的范围查询的加锁也有所不同,不同之处在于非唯一索引范围查询,索引的 next-key lock 不会有退化为间隙锁和记录锁的情况,也就是非唯一索引进行范围查询时,对二级索引记录加锁都是加 next-key 锁。

非索引查询

如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞

sql_safe_updates 设置为1,开启安全更新模式。

update 语句必须满足如下条件之一才能执行成功:

  • 使用 where,并且 where 条件中必须有索引列;
  • 使用 limit;
  • 同时使用 where 和 limit,此时 where 条件中可以没有索引列;