依赖改变

1
2
3
4
5
6

<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>

前置知识

进程和线程

进程就是: 一个软件实例就是一个进程

线程就是: 进程的子集

并发和并行

并发(concurrent)就是, 一个核心迅速在多个任务之间切换

并行(parallel)就是, 多个核心同时在多个任务运行。

异步和同步

同步: 不需要等待结果的返回

异步: 需要等待结果的返回’

  • 运行效率提升

    image-20231111190652846

创建和运行线程

直接使用Thread

1
2
3
4
5
6
Thread t = new Thread() {
public void run (){
// 需要执行的任务
}
};
t.start() //线程开始执行

Runnable 配合Thread

1
2
3
4
5
6
7
8
Runnable runnable = new Runnable() {
public void run () {
// 需要执行的任务
}
}
// 创建线程对象
Thread t = new Thread(runnable);
t.start()

可以试着使用lambda表达式把上诉代码精简

Thread 和 Runnable之间的关系

上诉的两个方法中

  • 方法一: 是把线程和任务合并在一起了
  • 方法二: 是把线程和任务分开了

并且使用 Runnable 可以更加的和线程池等高级api 配合。

FutureTask 配合Thread

可以获取任务的执行结果, 他和Runnable 有关系

1
2
3
4
5
6
7
8
9
10
// 创建任务对象
FutureTask<Integer>task3 = new FutureTask<>(()->{
log.debug("hello");
return 100;
});
// 第一个参数任务对象, 第二个参数线程名字
new Thread(task3, "t3")
// 这里的get会阻塞, 直到结果的返回
Integer result = task3.get();
log.debug("结果是:{}", result);

线程运行

查看进程线程的方法

image-20231111193656252

JConsole

  1. 打开cmd
  2. 输入JConsole
  3. 然后就可以连接你想要连接的Java服务

对于你想要监听的 类, 你需要做如下操作

1
2
3
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类

栈与栈帧

略(很简单)

多线程的栈与栈帧

每个线程都有自己独立的栈内存, 线程之间的栈互相不干扰

线程的上下文切换(Thread context Switch)

从使用cpu到不使用cpu的原因

image-20231112225156160

image-20231112225516807

常见方法

看pdf

start

getState 获取线程的状态

在未start之前, 调用线程的getState方法会得到一个NEW, 在调用start 之后, 调用线程的getState 方法会得到一个 RUNNABLE

  • 连续调用两次start 方法

image-20231112230727451

sleep

使得线程的状态由Running 变成 Timed Waiting 状态

线程打断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up...");
e.printStackTrace();
}
}
};
t1.start();

Thread.sleep(1000);
log.debug("interrupt...");
t1.interrupt();

报错信息如下

java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.test.Test7$1.run(Test7.java:14)

  • 同时我们还需要注意一个睡眠结束的线程未必会立即执行

sleep方法更新

1
2
3
4
// 原来
Thread.sleep(1000);
// 替换成这个更好
TimeUnit.SECONDS.sleep(1);

yield

使得线程从 running 变成 runnable 状态, 然后调用其他的线程, 如果没有其他的线程要执行会把机会继续让该线程进行

线程优先级(setPriority)

提示任务调度器去优先调度这个线程, 但是这只是个提示,不一定会执行

当cpu 繁忙的时候,优先级高的就会被分配尽可能多的服务

在Runnable 构造器中, 使用Thread.yield()可以实现让的效果

而setPriority 需要对线程对象 进行操作。

sleep的小应用

join

等待线程的结束, 拥塞主线程

  • 如果下面这段代码不加上 join 方法, 那么就会出现r = 0 . 但是又了join之后就会等到这个线程结束之后再去调用后面的方法
1
2
3
4
5
6
7
8
9
10
11
12
static int r = 0;
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
  • 带参数的join

设置等待的最大时间, 如果超过这个时间就不等待

interrupt

介绍不同阻塞状态被打断的会发生的事件

打断sleep join wait

  1. sleep 会抛出异常, 同时打断的那一刻会有一个打断标记, 但是会立刻清空,把它变成false
  • isInterrupted方法, 获取线程的打断标签(如果被正常的打断是为true)

如果在一个线程里面, 我们想要获得这个线程的实例

那么我们可以使用这个方法

Thread.currentThread()

获取打断标签 Thread.currentThread().isInterrupted()

两个阶段终止模式

线程T1 终止 线程 T2, 这里的终止, 不会立刻终止, 而是会等T2处理好一切之后再终止

  • 错误思路

    强制终止, stop 方法真正杀死线程。

    使用System.exit(int) 方法停止线程。

  • 正确思路

    image-20231113234824606

重新设置打断标记, 可以使得原本为false的标记变成true

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
import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TwoPhaseTermination")
public class Test13 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();

Thread.sleep(3500);
log.debug("停止监控");
tpt.stop();
}
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
// 监控线程
private Thread monitorThread;
// 停止标记
private volatile boolean stop = false;
// 判断是否执行过 start 方法
private boolean starting = false;

// 启动监控线程
public void start() {
synchronized (this) {
if (starting) { // false
return;
}
starting = true;
}
monitorThread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
// 是否被打断
if (stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
}
}
}, "monitor");
monitorThread.start();
}

// 停止监控线程
public void stop() {
stop = true;
monitorThread.interrupt();
}
}

打断park

park 可以让进程暂时停止

  • LockSupport.park()

  • 通过对线程使用interrupt方法, 可以让他停止暂停

  • 但是如果你打断了它一次, 再打断它就没用了(但是可以通过调用Thread.interrupted使得打断标记重置为假的)
    调用LockSupport.park(),开始打断

不推荐使用的方法 stop(使用两阶段终止模式来停止)、 suspend resume 。

守护线程

  • 用例: 垃圾回收器就是一个守护线程
  • Tomcat中的Acceptor 和 Poller 都是守护请求, 当它接受到shutdown命令之后,就会关闭掉这两个线程

线程的状态

五种状态

  • 初始状态

  • 可运行状态

  • 运行状态

  • 终止状态

  • 阻塞状态

    这是腿上面这几种状态的描述

image-20231114234427244

六种状态

image-20231114234947770

  • synchroniz 是一个锁操作,

统筹规划习题

  • 泡茶问题

方法一 : 使用join 方法

共享模型 之 管程

  • 问题引入

两个 线程同时加加减减, 然后停止, 但是结果和应该的结果不符合

image-20231115234729515

上面这个是 java的 自增和自减 运算符的 操作过程

临界区

多个线程访问共享资源

多个线程读写共享资源时发送了指令交错, 就会出现错误

在一段代码块如果存在对一个共享资源的多线程读写操作, 这个代码块就是一个临界区

可以理解为 一个代码块里面

1
2
3
static void increment {
counter++;
}

竞态条件

多个线程在临界区内执行, 由于代码块的执行顺序不同而导致结果没有办法预测,称作竞态条件

synchronized(对象锁)

语法

1
2
3
synchronized(对象) {
//临界区
}
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
public class Test17{
static int counter = 0;
static Object lock = new Object();
public static void main(String []args){

Thread t1 = new Thread(()->{
for(int i = 0; i < 5000; ++i){
synchronized (lock){
counter++;
}
}
}, "t1");
Thread t2 = new Thread(()->{
for(int i = 0; i < 5000; ++i){
synchronized(lock){
counter--;
}
}
})
}
t1.start();
t2.start();
t1.join();
t2.join();
log.info("{}", counter);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Test{
public synchronized static void test(){

}
}
//
class Test{
public static void test(){
synchronized(Test.class){ // 一般这里直接写Test.class = this就可以了

}
}
}

线程八锁

  • 类对象和 实例对象的区别

线程安全的判断

image-20231118004210879

  • 出现错误的情况

image-20231118233734764

  • 如果将list切换为局部变量的时候就不会有事情

image-20231118234049973

  • 但是如果局部变量暴露到外部就会有问题

当创建了子类之后, 并且方法为public ,子类通过添加线程,就有可能出现问题

如果你不想子类重写方法, 请在方法前面加上final修饰符

这就是这些修饰符的作用所在哦 ,这就是开闭原则中的闭原则

常见的线程安全类

可变的线程安全类

image-20231118234902899

虽然这些线程类中的每一个方法都实现了线程安全, 但是方法之间的组合不一定满足线程安全

参考如下示意图, 当然我们可以通过在外层在添加锁来解决这个问题

image-20231118235532720

不可变的线程安全类

获取你有疑问, 为什么replace ,substring也对字符串会有修改效果, 但是还是可以保证线程安全呢?

因为它并没有改变字符串里面的属性,他只是创建了一个新的字符串

线程安全的例题

  • 例题1

    image-20231119000606353

    spring没有特别注释, 都是单例模式,这时候会出现线程安全.,

    我们可以使用环绕通知, 将操作的线程变量变成局部变量

  • 例题2

    image-20231119000915153

    没有成员变量,且都是正常的局部变量, 所以是安全的

  • 例题3(我们要避免外星方法)

    image-20231119001359863

  • 总结

    加了final不一定可以保证线程安全

    需要使用上面两种类型才会安全

案例

买票问题

  • 这里暂时找不到源代码

转账问题

这里非常坑爹 ,这里的this 只会锁住自己的money, 不会锁住别人的钱

image-20231119003104800

实际上它的解决方法是将锁指定为 相同的class, 但是它的性能是不高的.(Account类对所有的对象都是共享的.我就拿出这些共享的东西来作为锁)

image-20231119003235982

Monitor

java通过Klass Word 找到它的类对象

image-20231121232402061

image-20231121232138066

之前我们说的锁就是Monitor, 也就是监视器或者管程

  • 上锁的原理
  • 这是上锁的流程

image-20231121233203681

image-20231121233145867

  • 下面是从**==字节码角度==**理解锁的竞争

    过于难理解 ,不贴了

synchronize优化原理

深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁_偏向锁升级为重量级锁-CSDN博客

轻量级锁

如果一个对象虽然有多个线程访问,但多线程访问的时间是错开的, 那么就可以升级为轻量锁

语法仍然是synchronized, 和重量级锁相似

image-20231121235349598

cas表示交换操作, 是原子性

image-20231121235402033

cas 操作失败会分为两种

第二种是 自己的线程 给自己加锁, 就会产生锁重入, 如下面这种程序

1
2
3
4
5
6
7
8
9
10
11
12
13
static final obj = new Object();
public static void method1() {
synchronized (obj){
// 同步代码块1
method2();
}

}
public static void method2() {
synchronized(obj ){
//同步代码块2
}
}

image-20231121235708042

image-20231121235901700

锁膨胀

image-20231125231209322

image-20231125231244784

自旋优化
  • 适合多核CPU下面才有意义

image-20231125231548714

java 7 之后就不能控制是否开启自旋

偏向锁

image-20231125234729547

  • 没有使用偏向锁

image-20231125233140429

  • 使用了偏向锁

image-20231125233111619

  1. 偏向状态

    image-20231125233801290

    偏向锁会延迟生效, 所以需要过一会才会从正常状态变成偏向锁状态

    我们可以通过在jvm 加上配置参数-XX:BiasedLockingStartupDelay=0来禁止延迟

  2. 当一个可以偏向的锁,使用hashcode之后, 就会变成无锁状态

  3. 如果存在一个其他线程也需要使用偏向锁对象的时候, 偏向锁也会变成轻量锁

  4. 当调用wait/notify 的时候就会撤销偏向锁和轻量锁给撤销, 变成重量级锁

批量重偏向
  • 当撤销出现20次

image-20231125235827458

如果有一个锁, 一开始由线程a掌控, 后面转变为 线程b掌控, 那么在线程b咋锁重入多次之后, 就会变成 偏向锁

批量撤销

image-20231126000436138

  • 当撤销出现40次

锁消除

利用 JIT(即使编译器, 会对java的字节码进行进一步的优化) 会优化锁(反正是一些 jvm的骚操作就对了)

image-20231126001743330

image-20231126002030539

wait

image-20231201234554894

API 介绍

image-20231201234715382

  • 我们可以往 wait ( )里面添加一个时间, 这样他会在过了这个时间之后就唤醒

wait 和 sleep 的区别

sleep 睡眠的时候, 不会释放锁, 但是wait 会释放锁

sleep 不需要和 synchronize 配合使用, 但是wait 需要

sleep 是Thread 方法, wait 是Object 的方法

他们的共同点是 : 它们的状态都是TIMED_WAITING

实验总结

我们可以设置一个 唤醒的条件为 xxx, 如果确实是对应线程的唤醒就把xxx 改成true, 然后跳出while 循环。

image-20231202001559648

  • 唤醒条件。

image-20231204211131174

  • 唤醒线程

image-20231204211203415

模式

同步模式

保护模式

image-20231204211329108

  • 保护模式大体上是这样实现的

image-20231204212124193

image-20231204212817002

使用join 就需要一直等待, 但是使用这个模式可以, 继续干别的

  • 我们还可以给这个模式添加一些功能

    • 增添超时时间

      image-20231204213040958

      但是这里会有点问题, 如果出现虚假唤醒的时候, 你每次又得 重新等2秒, 所以修改为下面这种方式

      image-20231204213336680

join 原理

略(类似于上面超时的保护模式)

保护模式-拓展2

image-20231204214459811

有点懵逼(到时候可以在看看)

异步模式 - 生产者消费者

image-20231204221816973

image-20231204222624029

image-20231204222632387

  • 测试代码

image-20231204223105216

  • 产生的结果为

image-20231204223118306

park & Unpark

  • park 就是 等待, 当调用unpark的时候就唤醒park的地方
  • 如果先调用unpark的时候, park 就不会等待

差异

image-20231208220812459

原理

  • 但是多次unpark只会补充一次备用干粮

image-20231208221103307

重新理解线程状态

image-20231208221917441

image-20231208221929578

image-20231208222553708

image-20231208222930962

image-20231208223043747

image-20231208223103215

多把锁

引入

就是把原本一个锁换成多个锁, 缩小锁粒度

image-20231208223350900

死锁

a锁想要b锁

b锁想要a锁

定位死锁

1
2
3
4
5
6
// 在terminal中输入,查看线程信息
jps
// xxx 代表线程号
jstack xxx
// jmeter连接对应的进程

哲学家就餐

image-20231208224001123

每个人拿一个筷子, 但是在等别人的筷子