JUC并发编程

Time: 2024-12-22 Sunday 02:08:02
Author: Jackasher

JUC并发编程

Synchronized原理

故事角色

老王 - JVM

小南 - 线程

小女 - 线程

房间 - 对象

房间门上 - 防盗锁 - Monitor

房间门上 - 小南书包 - 轻量级锁

房间门上 - 刻上小南大名 - 偏向锁

批量重刻名 - 一个类的偏向锁撤销到达 20 阈值

不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,

即使他离开了,别人也进不了门,他的工作就是安全的。

但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女

晚上用。每次上锁太麻烦了,有没有更简单的办法呢?

小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因

此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是

自己的,那么就在门外等,并通知对方下次用锁门的方式。

后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍

然觉得麻烦。

于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那

么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦

掉,升级为挂书包的方式。

同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老

家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老

王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字

后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包

栈帧

image-20241119230150126

Monitor

![image-20241120102950292](/Users/leojackasher/Library/Mobile Documents/com~apple~CloudDocs/Markdown/JUC并发编程.assets/image-20241120102950292.png)

MarkWord

image-20241120103037738

轻量级锁

image-20241121162325138

偏向锁

偏向锁前面的是线程id,而轻量级锁前面是锁记录地址,重量级锁前面是monitor锁地址,偏向锁加锁的方式就是直接表明线程id

image-20241121174830176

为什么使用hashcode会禁用偏向锁

因为会把位用于方hashcode,没有位置再放54位的线程id了

那为什么不会禁用其他的锁呢?

因为轻量级锁会把hashcode存在栈帧里面,而重量级锁会存在monitor里面

偏向锁撤销

如果有冲突,会被升级为轻量级锁

image-20241121180103765

锁对性能的影响

忽略掉JIT的优化后,竟然差了17倍!

image-20241121181956916

“Guarded Suspension”(保护性暂停)设计模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package day01;

/**
* @author Jackasher
* @version 1.0
* @className GuardObject
* @since 1.0
**/
public class GuardObject {
private Object response;

public synchronized Object get() throws InterruptedException {
if (response == null) {
wait();
}
return response;
}

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

}

虚假唤醒

假唤醒(Spurious Wakeup)是多线程编程中使用 wait 和 notify 时可能出现的现象。即使没有调用 notify 或 notifyAll,线程可能也会被唤醒。这种行为是操作系统和 JVM 的实现细节所导致的。

原因:操作系统和 JVM 的实现细节

​ 1.信号丢失或竞争

在多线程环境中,wait 和 notify 是基于底层操作系统的同步原语实现的。在某些情况下,由于线程调度或者信号竞争,可能会发生信号丢失或额外的唤醒。

​ 2.硬件中断和系统优化

某些硬件中断或系统优化(比如节省唤醒信号的成本)会触发线程错误地离开等待状态。

​ 3.POSIX 标准

POSIX 线程标准允许线程被无故唤醒,Java 的 wait 和 notify 实现也继承了这一行为。因此,Java 开发者需要主动编写防御性代码来处理虚假唤醒。

​ 4.其他线程操作影响

某些情况下,其他线程可能在没有显式调用 notify 的情况下,导致等待线程恢复运行(例如 JVM 的垃圾回收暂停和恢复调度)。

为了应对虚假唤醒,最佳实践是将 wait 放在循环(while)中,而不是条件判断(if)中

如何让线程循环打印abc

线程分别打印,abc,那么每个线程知道自己什么时候打,例如b需要知道a打印完后给一个信号,这个信号可以设为flag,每个线程需要的flag不一样,先给a一个flag=1开始,打印后,设为2传给b线程,

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
package wait_ify;

/**
* @author Jackasher
* @version 1.0
* @className WaitNotify
* @since 1.0
**/
public class WaitNotify {
private int flag;

public int getFlag() {
return flag;
}

public void setFlag(int flag) {
this.flag = flag;
}

public void print(String printContent, int currentFlag, int nextFlag) {

while (true) {
synchronized (this) {
if (flag == currentFlag) {
System.out.println(printContent);
flag = nextFlag;
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}
}

}

指令重排

这段代码是有问题的,首先我们要弄清楚INSTANCE = new Singleton()在java虚拟机里面是如何操作的,通常来说,是先new dup init putstatic 但是jvm可能颠倒init和putstatic的顺序,先给地址后初始化,这个时候如果有第二个线程进来,就会判断错误image-20241124152422291

volatile

1. 读屏障(Load Barrier)

​ •在读取 volatile 变量前,会插入一条指令,确保在当前线程从主内存加载该变量值之前,清空当前线程工作内存中缓存的共享变量。这保证了读取的值是最新的。

2. 写屏障(Store Barrier)

​ •在写入 volatile 变量后,会插入一条指令,确保将该变量的新值立即刷新到主内存,并让其他线程的缓存失效,保证其他线程可以看到最新值。

CAS(CompareAndSet)

CAS提供了不上锁的方法实现线程安全,原理是利用操作系统的原子操作,比较和赋值为同时操作,CAS的底层其实就是Volatile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
* @author Jackasher
* @version 1.0
* @className Cas
* @since 1.0
**/
public class Cas {
public static void main(String[] args) {
AtomicInteger balance = new AtomicInteger(100);
int i = balance.get();
balance.compareAndSet(i, 110);
System.out.println(balance);
}
}

Integer的线程不安全

当把int换成Integer后,加减就会变成线程不安全的 balance -= amount;,

​ •int 是 Java 的基本数据类型,操作 int 是原子性的(单一操作不会被线程中断,如简单的加减赋值)。

​ •Integer 是 Java 的引用类型,不可变对象(immutable)。每次对 Integer 的值进行操作,实际上是创建了一个新的 Integer 对象,而不是修改原来的值。

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
package cas2;

/**
* @author Jackasher
* @version 1.0
* @className Account
* @since 1.0
**/
public class Account {
private Integer balance;

public Integer getBalance()
{
return balance;
}

public void deposit(int amount)
{
balance += amount;
}

public void withdraw(int amount)
{
balance -= amount;
}

public void setBalance(int balance)
{
this.balance = balance;
}

@Override
public String toString() {
return "Account{" +
"balance=" + balance +
'}';
}
}

手写AtomicInteger

核心就是开始操作前都要检查值对不对,对了再操作,CAS来完成这个比较值和赋值的操作

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
package cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
* @author Jackasher
* @version 1.0
* @className Atomic
* @since 1.0
**/
public class Atomic {
public static void main(String[] args)
{
AtomicInteger atomicInteger = new AtomicInteger(100);
int i = atomicInteger.updateAndGet(oldValue -> oldValue + 100);
System.out.println(i);
System.out.println(atomicInteger.get());

updateAndGet(atomicInteger);
System.out.println(atomicInteger.get());
}

public static void updateAndGet(AtomicInteger atomicInteger)
{
int expectedValue = atomicInteger.get();
int newValue = expectedValue + 100;
while (true) {
if (atomicInteger.compareAndSet(expectedValue, newValue)) {
break;
}
}

}
}

Consumer和Supplier

黑马突然搞出这两个,我也很懵逼,其实就是java8的lambda表达式,function函数

Supplier实例

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.function.Supplier;

public class SupplierExample {
public static void main(String[] args) {
// 使用Supplier生成一个随机数
Supplier<Double> randomSupplier = () -> Math.random();

// 调用get方法生成数据
System.out.println("随机数1: " + randomSupplier.get());
System.out.println("随机数2: " + randomSupplier.get());
}
}

Consumer实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.function.Consumer;
import java.util.Arrays;
import java.util.List;

public class ConsumerExample {
public static void main(String[] args) {
// 创建一个Consumer打印数据
Consumer<String> printConsumer = (str) -> System.out.println("处理数据: " + str);

// 调用accept方法消费数据
printConsumer.accept("Hello, Consumer!");

// 在集合中应用Consumer
List<String> items = Arrays.asList("A", "B", "C");
items.forEach(printConsumer); // 遍历并消费每个元素
}
}

手写线程池

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
package thread_pool;

/**
* @author Jackasher
* @version 1.0
* @className WorkerThread
* @since 1.0
**/
public class WorkerThread extends Thread {
private Runnable task;
private BlockingQueue<Runnable> blockingQueue;
private ThreadPool threadPool;
private boolean isRunning = true;


public WorkerThread(Runnable task, BlockingQueue<Runnable> blockingQueue) {
this.task = task;
this.blockingQueue = blockingQueue;

}

@Override
public void run() {
while (isRunning) {
try {
if (task != null) {
task.run();
}
if((task = blockingQueue.take()) != null) {
task.run();
}

} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}

System.out.println("准备退出");
synchronized (threadPool.getWorkers()) {
System.out.println("线程:" + Thread.currentThread().getName() + " 退出");
threadPool.getWorkers().remove(this);
}
}
}

我前面发现了这个地方,无法执行,会阻塞,原来是因为队列没有释放锁

1
2
3
if((task = blockingQueue.take()) != null) {
task.run();
}

JUC并发编程
http://example.com/2024/12/22/JUC并发编程/
作者
Jack Asher
发布于
2024年12月22日
许可协议