并发-并发编程的挑战
Published in:2023-04-02 | category: 学习

同步VS异步

同步和异步用来形容一次方法调用。

同步:同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。

异步:异步调用指的是,调用者不管被调用的方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。

并发VS并行

并发:指多个任务交替执行

并行:真正意义上的“同时运行”。

实际上,系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

阻塞VS非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响。

阻塞:一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起。

非阻塞:没有一个线程可以阻塞其他线程,所有的线程都会尝试地前行。

临界区

一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

并发编程

优点:

1、并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。

2、面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分。

缺点:并发编程的目的是为了让程序运行的更快,但是也会造成一些问题:上下文切换,线程安全

上下文切换

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。因为时间片非常短,所以CPU通过不断地切换线程执行,让我们感觉多个线程是同时执行的。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

多线程一定快吗?

不一定,因为线程有创建和上下文切换的开销。

如何减少上下文切换?

1、无锁并发编程。可以参考concurrentHashMap锁分段的思想,不同线程处理不同段的数据,这样在多线程竞争条件下,可以减少上下文切换的时间。

2、CAS算法。利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效减少一部分不必要的锁竞争带来的上下文切换。

3、使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。

4、协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

死锁

一旦产生死锁,就会造成系统功能的不可用。

模拟死锁的代码:

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
public class DeadLockDemo {

private static String A = "A";
private static String B = "B";

public static void main(String[] args){
new DeadLockDemo().deadLock();
}

private void deadLock(){
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
//对A加锁
synchronized(A){
System.out.println("t1 get resource A");
try {
//睡眠一段时间,让t2先获取到B
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
//尝试获取B,对B加锁,因为t2一直在尝试获取A,一直不释放B,所以t1获取不到B
synchronized(B){
System.out.println("t1 get resource B");
System.out.println("1");
}
}
}
});
Thread t2 =new Thread(new Runnable() {
@Override
public void run() {
//对B加锁
synchronized (B){
System.out.println("t2 get resource B");
//获取A,对A加锁,因为t1一直在尝试获取B,没有获取到一直未执行结束,不释放A,所以t2不会获取到A
synchronized(A){
System.out.println("t2 get resource A");
}
}
}
});
t1.start();
t2.start();
}
}

避免死锁常用的方法:

1、避免一个线程同时获取多个锁。

2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

3、尝试使用定时锁,使用lock.tryLock(timeout)来代替使用内部锁机制。

4、对数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

Prev:
并发-Java并发机制的底层实现原理
Next:
JUC-共享模型之内存