为什么你总是搞不定并发?

104 阅读4分钟

关注微信公众号 程序员小胖 每日技术干货,第一时间送达!

并发是什么?

标题看起来是不是很蒙,虽然你知道是什么,但是一下子很难用语言清晰的回答出这个问题。

其实很简单举个🌰你就明白了。

为啥你可以一边写代码一边听歌?

为啥你可以一边打台球一边跟别人聊天?

这些行为是不是都属于并发行为?同样的计算机也支持同样的并发操作

并发(concurrency)

是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。 在一个时间段的线程代码运行时,其它线程处于挂起状。

具体怎么实现的呢?它是依赖于cpu的时间片轮转算法实现的呢?

时间片轮转算法(Round Robin Scheduling)

是一种抢占式的调度算法,常用于多道程序设计的操作系统中。它的核心思想是按照顺序将 CPU 时间分成若干个时间片,每个进程在一个时间片内执行,当时间片用完,系统将 CPU 分配给下一个进程。

接下来我们用一段代码演示下该算法

public class RoundRobinScheduler {
    private Process[] processes;
    private int currentProcess;
 
    public RoundRobinScheduler(Process[] processes) {
        this.processes = processes;
        this.currentProcess = 0;
    }
 
    public Process getNextProcess() {
        if (currentProcess >= processes.length) {
            currentProcess = 0;
        }
        Process process = processes[currentProcess];
        currentProcess++;
        return process;
    }
 
    public static void main(String[] args) {
        Process p1 = new Process("P1");
        Process p2 = new Process("P2");
        Process p3 = new Process("P3");
        RoundRobinScheduler scheduler = new RoundRobinScheduler(new Process[]{p1, p2, p3});
 
        for (int i = 0; i < 10; i++) {
            Process process = scheduler.getNextProcess();
            System.out.println("Running process: " + process.getName());
        }
    }
}
 
class Process {
    private String name;
 
    public Process(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
}

这个简单的Java代码示例展示了如何实现一个简单的时间片轮转(Round Robin, RR)调度算法。它定义了一个RoundRobinScheduler类,它维护了一个进程数组和一个当前进程的索引。getNextProcess方法用于选择下一个应该运行的进程,并且会在进程数组中循环。这个例子可以作为操作系统中进程调度算法教学的一个简单示例。

并发的特性

原子性

原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。

代码示例

private long count = 0public void calc() { 
  count++; 
}
  1. 将count从主存读到工作内存中的副本中
  2. +1的运算
  3. 将结果写入工作内存
  4. 将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)

那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据 关键字 synchronized。

可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

代码示例

//线程1 
boolean stop = falsewhile(!stop){
  doSomething(); 
}
//线程2
stop = true;

若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

有序性

虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按 照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对 变量的值没有造成影响,但有可能会出现线程安全问题。

代码示例

int a = 0bool flag = false;
public void write() {
    a = 2;  //1 
    flag = true//2
}
public void multiply() { 
  if (flag) { //3 
  int ret = a * a;//4 
  } 
}

write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果, 再到线程1,这时候a才赋值为2,很明显迟了一步