3月14日工作内容

First Post:

Last Update:

3月14日工作内容

前端判断出错导致编辑时出现误导提醒

image-20240314172149617

昨天晚上客户提到:

在公司管理员工列表页面,点击编辑,明明是赵X的账户,想调整一下接数据的数量,然后点确认跳出来的却是该员工是曹X

检查没有出现请求错误,并且确定返回参数的职级关系无误之后,把问题反馈给前端,就没我事了。

今天大多时间没什么任务,我一边复习多线程。

Java复习

线程

线程挺抽象的,一直很混淆,在这篇文章中弄清楚了一些:

1.9 多线程编程 (yuque.com)

同步锁

看过很多锁的例子,说句实在的,很难把握得住,很多时候看起来很像的代码运行起来结果都不一样,所以我打算记住一两种就够了。

通过synchronized(lock)的方式来加锁,其实是在锁对象(可以是任意对象最简单的如private byte[] lock = new byte[0];)加入一个标识符,当执行同步括住的代码的时候,标识符不让其他线程使用该锁对象,只有代码块执行结束了才会释放该对象。

实际在操作的时候,要保证的一点就是各线程中锁对象是同一个。

而将锁放在实现Runnable接口的类中,通过传入该Runnable类实例化不同的线程,这样子创建出来的线程就是具有锁的功能的。

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 SynchronizedDemo implements Runnable{
private int count = 100;
private byte[] lock = new byte[0];
@Override
public void run() {
while (true){
synchronized (lock) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了一张票,余票:" + --count);
}
}
}
}

public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
Thread thread1 = new Thread(synchronizedDemo, "窗口一");
Thread thread2 = new Thread(synchronizedDemo, "窗口二");
Thread thread3 = new Thread(synchronizedDemo, "窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}

运行结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
窗口二卖出了一张票,余票:20
窗口三卖出了一张票,余票:19
窗口二卖出了一张票,余票:18
窗口一卖出了一张票,余票:17
窗口三卖出了一张票,余票:16
窗口三卖出了一张票,余票:15
窗口一卖出了一张票,余票:14
窗口二卖出了一张票,余票:13
窗口二卖出了一张票,余票:12
窗口一卖出了一张票,余票:11
窗口三卖出了一张票,余票:10
窗口三卖出了一张票,余票:9
窗口一卖出了一张票,余票:8
窗口二卖出了一张票,余票:7
窗口二卖出了一张票,余票:6
窗口一卖出了一张票,余票:5
窗口三卖出了一张票,余票:4
窗口二卖出了一张票,余票:3
窗口三卖出了一张票,余票:2
窗口一卖出了一张票,余票:1
窗口三卖出了一张票,余票:0

微小的改动下,这个代码就得出不一样的结果:

比如这个,就没办法分配给其他线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SynchronizedDemo implements Runnable{
private int count = 100;
private byte[] lock = new byte[0];
@Override
public void run() {
synchronized (lock) {
while (count>0){
System.out.println(Thread.currentThread().getName() + "卖出了一张票,余票:" + --count);
}
}
}

public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
Thread thread1 = new Thread(synchronizedDemo, "窗口一");
Thread thread2 = new Thread(synchronizedDemo, "窗口二");
Thread thread3 = new Thread(synchronizedDemo, "窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
窗口一卖出了一张票,余票:20
窗口一卖出了一张票,余票:19
窗口一卖出了一张票,余票:18
窗口一卖出了一张票,余票:17
窗口一卖出了一张票,余票:16
窗口一卖出了一张票,余票:15
窗口一卖出了一张票,余票:14
窗口一卖出了一张票,余票:13
窗口一卖出了一张票,余票:12
窗口一卖出了一张票,余票:11
窗口一卖出了一张票,余票:10
窗口一卖出了一张票,余票:9
窗口一卖出了一张票,余票:8
窗口一卖出了一张票,余票:7
窗口一卖出了一张票,余票:6
窗口一卖出了一张票,余票:5
窗口一卖出了一张票,余票:4
窗口一卖出了一张票,余票:3
窗口一卖出了一张票,余票:2
窗口一卖出了一张票,余票:1
窗口一卖出了一张票,余票:0

再比如这个,偶尔会出现买多了的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SynchronizedDemo implements Runnable{
private int count = 100;
private Object lock = new Object();
@Override
public void run() {
while (count>0) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "卖出了一张票,余票:" + count--);
}
}
}

public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
Thread thread1 = new Thread(synchronizedDemo, "窗口一");
Thread thread2 = new Thread(synchronizedDemo, "窗口二");
Thread thread3 = new Thread(synchronizedDemo, "窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
窗口一卖出了一张票,余票:20
窗口三卖出了一张票,余票:19
窗口一卖出了一张票,余票:18
窗口二卖出了一张票,余票:17
窗口一卖出了一张票,余票:16
窗口三卖出了一张票,余票:15
窗口二卖出了一张票,余票:14
窗口三卖出了一张票,余票:13
窗口二卖出了一张票,余票:12
窗口一卖出了一张票,余票:11
窗口三卖出了一张票,余票:10
窗口二卖出了一张票,余票:9
窗口一卖出了一张票,余票:8
窗口二卖出了一张票,余票:7
窗口一卖出了一张票,余票:6
窗口三卖出了一张票,余票:5
窗口一卖出了一张票,余票:4
窗口二卖出了一张票,余票:3
窗口三卖出了一张票,余票:2
窗口二卖出了一张票,余票:1
窗口一卖出了一张票,余票:0
窗口三卖出了一张票,余票:-1

对于这两种异常情况的解释,GPT是这样说的:

在同步块中使用 while 循环和将 while 循环条件放在同步块之外然后在 while 循环中使用 synchronized,这两种方式的区别在于锁的释放时机和锁的粒度。

  1. **将 while 循环条件放在同步块之外然后在 while 循环中使用 synchronized**:

    1
    2
    3
    4
    5
    javaCopy codesynchronized (lock) {
    while (count > 0) {
    // 操作 count
    }
    }

    这种方式下,线程进入同步块后首先获取锁,然后检查 count 的值,如果条件满足则执行相应的操作,否则退出同步块。在这种情况下,锁的粒度比较粗,因为锁的持有范围是在整个 while 循环内。这可能会导致其他线程在等待锁时被阻塞,即使它们只是想读取 count 的值。

  2. while 循环放在同步块内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    javaCopy codewhile (true) {
    synchronized (lock) {
    if (count > 0) {
    // 操作 count
    } else {
    break;
    }
    }
    }

    这种方式下,线程进入 while 循环后首先获取锁,然后检查 count 的值,如果条件满足则执行相应的操作,否则退出 while 循环。在这种情况下,锁的持有范围是在每次迭代中,因此锁的粒度更细。这意味着即使某个线程持有了锁,其他线程仍然可以读取 count 的值,而不需要等待整个 while 循环结束。

在第二种方式中,每次迭代时,线程都会释放锁,这样其他线程就有机会获得锁并进行操作。这有助于减少竞争和提高并发性能,并且能够避免出现负数的情况,因为每个线程在获取锁之后都会检查 count 的值,如果小于等于 0,则会退出循环。

记法就是细粒化到同步锁住里面的每一次操作都做一次判断

线程通信

image-20240314152004769

  1. 使用 wait() 使方法进入等待状态

  2. 使用 notify() 唤醒等待的线程

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
package singleTest.threadTest;

/**
* @author Zepore
* @version 1.0
* @description: TODO
* @date 2024/3/14 15:07
*/
public class Egg {
private boolean hasEgg = false;
private byte[] lock = new byte[0];
Thread pickEgg = new Thread(()->{
synchronized (lock) {
while (true) {
if (hasEgg) {
System.out.println("捡到鸡蛋了");
hasEgg = false;
} else {
System.out.println("等另一线程丢鸡蛋");
try {
lock.wait();//必须再同步代码块中写
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}, "捡鸡蛋线程");
Thread throwEgg = new Thread(()->{
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock) {
System.out.println("生鸡蛋了");
hasEgg = true;
lock.notify();
}
}
}, "生鸡蛋线程");

public static void main(String[] args) {
Egg egg = new Egg();
egg.pickEgg.start();
egg.throwEgg.start();

}
}

wait()sleep()的区别

image-20240314152051627

刚好刷到了两道跟线程通信相关的常见面试题:

交叉打印的实例

线程通信可以用于交叉打印:

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
package singleTest.threadTest;

import cn.hutool.crypto.GlobalBouncyCastleProvider;

/**
* @author Zepore
* @version 1.0
* @description: TODO
* @date 2024/3/14 15:45
*/
public class CrossPrint implements Runnable{
private byte[] lock = new byte[0];
private int num = 0;
private final int CAPACITY = 100;

@Override
public void run() {
while (true) {
synchronized (lock) {
lock.notify();
if (num < CAPACITY) {
System.out.println("Print[" + ++num + "]: Thread-" + Thread.currentThread().getName());
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

} else break;
}
}
}

public static void main(String[] args) {
CrossPrint crossPrint = new CrossPrint();
Thread thread1 = new Thread(crossPrint, "线程一");
Thread thread2 = new Thread(crossPrint, "线程二");
thread1.start();
thread2.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
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
package singleTest.threadTest;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
* @author Zepore
* @version 1.0
* @description: TODO
* @date 2024/3/14 15:59
*/
public class BlockQueue {
private final int CAPACITY = 20;
private List<Integer> queue = new ArrayList<>(CAPACITY);
private byte[] lock = new byte[0];
public void produce(int num) {
synchronized (lock) {
lock.notify();
if(queue.size() < 20) {
System.out.println("队列长度:["+queue.size()+"],新增元素:["+num+"],当前线程:["+Thread.currentThread().getName()+"]");
queue.add(queue.size(),num);
} else {
System.out.println("队列已满,["+Thread.currentThread().getName()+"]线程wait");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public void consume() {
synchronized (lock) {
lock.notify();
if(queue.size()>0) {
System.out.println("队列长度:["+queue.size()+"],消费元素:["+queue.remove(0)+"],当前线程:["+Thread.currentThread().getName()+"]");
} else {
System.out.println("队列为空,["+Thread.currentThread().getName()+"]线程wait");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

public static void main(String[] args) {
BlockQueue blockQueue = new BlockQueue();
new Thread(()->{
while (true){
blockQueue.produce(ThreadLocalRandom.current().nextInt());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"生产者线程").start();
new Thread(()->{
while (true){
blockQueue.consume();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"消费者线程").start();
new Thread(()->{
while (true){
System.out.println("当前队列长度:[" + blockQueue.queue.size() + "],当前线程:[" + Thread.currentThread().getName() + "]");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"监控线程").start();
}
}

异步锁

文章中没有提到异步锁,我找了其他文章看一看了:

同步(Synchronous)和异步(Asynchronous) - 小cai一碟这篇文章中解释了同步与异步 的区别:

同步和异步强调的是消息通信机制 (synchronous communication/ asynchronous communication)。所谓同步,就是在发出一个”调用”时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。而异步则是相反,”调用”在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在”调用”发出后,”被调用者”通过状态、通知来通知调用者,或通过回调函数处理这个调用

Java中的Atomic类提供了一组原子操作来实现异步锁:

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
package singleTest.threadTest;

import java.util.concurrent.atomic.AtomicInteger;

/**
* @author Zepore
* @version 1.0
* @description: TODO
* @date 2024/3/14 17:08
*/
public class AtomicDemo {
private AtomicInteger count = new AtomicInteger(0);
public int increment() {
return count.incrementAndGet();
}
public int decrement() {
return count.decrementAndGet();
}
public int getValue() {
return count.intValue();
}

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

Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicDemo.increment();
}
System.out.println(Thread.currentThread().getName() + ":结果:【" + atomicDemo.getValue() + "】");
}, "加法线程");

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicDemo.decrement();
}
System.out.println(Thread.currentThread().getName() + ":结果:【" + atomicDemo.getValue() + "】");
}, "减法线程");

try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":结果:【" + atomicDemo.getValue() + "】");
}
}

结果是:

1
2
3
减法线程:结果:【0】
加法线程:结果:【0】
main:结果:【0】