并发冲突分析与解决
我们都知道,我们计算机中的运算都是交给CPU处理的,如果对于一个单核CPU来说,在绝对的某个时刻内,CPU只能同时完成一个任务,不可能同时的处理多个任务。但是我们都知道,我们的计算机并不是只能同时运行一个程序,我们往往是多个应用一起运行,比如我们经常一边登着QQ,一边使用着浏览器,而且还一边听着音乐,而且在电脑的后台往往还运行着许多的程序,那我们的CPU是怎么处理这些任务的,让他们 “同时” 运行的呢?
这里我们想要理解两个概念:并行 和 并发
- 并行:两个任务真正的同时进行,比如一个CPU是双核的,那么他就可以同时运行两个任务,我们也叫并行的运行两个任务。
- 并发:两个任务看似是“同时”在发生,但是实际上是在交替进行,同一个时刻CPU实际上只能处理一个任务。
我们现在的电脑的CPU基本上是多核的,比如一个八核的电脑,就可以并行的处理八个任务,但是我们知道我们电脑上的任务往往远远超过8个,比如图中,我的电脑现在的进程数就有112个,那每个进程的线程数肯定有更多。
CPU需要“同时”处理那么多的任务,就需要每个任务交替进行,这就叫做 “并发执行”。我们在启动了线程后,就会把线程的执行交给操作系统,操作系统会根据当前CPU的状态来安排不同的线程执行,CPU的将时间分为一个个的 时间片,每个线程都会分到相应的时间片,CPU会交替的处理各个任务,当时间片到了,就会转向另外的线程,这样在宏观的角度看上去,好像是多个任务 “同时” 在进行。这就是最基本的 并发执行。
但是众所周知,现在我们的计算机都被互联网连接了起来,现在比如我们去访问一个网站(比如CSDN),我们的请求不再是被某台计算的CPU进行处理了,因为当流量达到一个很大的量级时,每时每刻都会用成百数千个请求去访问CSDN这个网站,这个时候的请求不是一般的个人服务器能解决的了,现在企业运用的处理技术基本都是 “分布式” 数据存储和计算,也就是我们平时经常听到的 “云计算”。
当我们的网站在一瞬间收到了几百万个请求时,我们的服务器会在一瞬间承受很大的负荷(比如双十一),这种情况就被我们称为 “高并发”:也就是一瞬间服务器要并发的处理大量的请求任务。
我们在日常的自行调试中基本上很难遇到 “高并发” 的情况,但是我们今天要讨论的 “并发冲突” 却是很容易遇到的,而且就算是 高并发 中也必须考虑并解决 并发冲突 这样一个很重要的问题,这样才能保证系统的正常运行。
“并发冲突” 这个问题其实非常好理解,其实就是这样的一个问题:比如你的银行里有100元钱,你想把钱取出来,你有两种方式可以取钱:去柜台 和 手机取款。那我们可以考虑这样的一种情况,如果你找一个你的朋友用你的手机取钱,你去柜台取款。那因为系统处理数据总是需要时间,如果你的朋友可以在你去柜台取完100元钱的瞬间,就通过手机取款,这个是时候系统还在没有刷新,是不是你的朋友就又可以取出100元钱了呢?
其实这就是 “并发冲突” 需要解决的问题,就是当两个线程同时需要处理同一个 共享数据 时,怎么样才能保证数据处理不出错误,这两个线程的处理怎样才能不互相冲突,当这一套逻辑运用到 “高并发” 的系统上时,就是需要解决,当很多请求同时出现,并且要对我的 “分布式” 数据系统进行访问或者修改时,我怎么才能保证处理的结果不出差错,符合预期。
下面我通过具体的代码来模拟这样的情况:
import java.util.ArrayList;
public class Concurrency implements Runnable{
//十个线程共同来操作count,让count--
int count = 100;
@Override
public void run() {
while (count > 0) {
count--;
System.out.println(Thread.currentThread().getName() + " count: " + count);
}
}
public static void main(String[] args) {
//线程操作的共同对象
Concurrency con =new Concurrency();
//创建线程
for(int i = 0; i < 10; i++) {
Thread th1 = new Thread(con);
th1.start();
}
}
}
程序的运行结果如下:
从结果我们可以看出,十个线程的启动并不是按照顺序的,而是我们 start() 以后,交给操作系统自行决定的,而且从结果我们也可以看出几个线程是交替运行的,而且我们可以看到 “98” 出现了两次,本来一个数据一直递减,同一个数字是不可能出现两次的,但是这里 “98” 出现了两次,说明 Thread-0 与 Thread-1 都获取到了count = 98,这个数据,也就是 “同时取到了两份钱” !这就是多线程在处理同一个数据时,发生了 “并发冲突”;
那我们这里如何解决这个问题呢?解决这个问题的方法是多样的:
- 我们可以给线程加 锁,直接运用Java并发包中的锁类,比如 实现接口Lock,就能起到给线程加锁的作用:也就是同一个时间,只i能获得 锁 的一个线程来处理 共享数据,其他线程必须进入阻塞态,等到该线程处理完成后,再来争夺锁的控制权。
- 也可以利用 synchronized 包围的同步代码块来解决多线程之间的 并发冲突,这里我选择的就是这种方式,下面以具体代码为例:
-import java.util.ArrayList;
public class Concurrency implements Runnable{
int count = 100;
@Override
public void run() {
/**
与上面代码唯一的区别,用synchronized将每个线程都会调用的,发生 冲突的代码块包裹,这样同一个时间就只用一个线程能够运行该段代码。
*/
synchronized (this) {
while (count > 0) {
count--;
System.out.println(Thread.currentThread().getName() + " count: " + count);
}
}
}
public static void main(String[] args) {
Concurrency con =new Concurrency();
//创建线程
for(int i = 0; i < 10; i++) {
Thread th1 = new Thread(con);
th1.start();
}
}
}
加入同步代码块之后,运行的结果如下:
可以看到当Thread-0这部分代码获取到同步代码块的控制权以后,其他线程进入阻塞状态,必须等Thread-0释放了对锁的控制权以后,其他的线程才能进入! 这样,就解决了多线程中 并发冲突 的问题!
其实,在Java并发包的底层同样也是利用了synchronized关键字,只不过是锁类对其具体的代码做了进一步的封装,更加方便使用。
当然解决 “并发冲突” 的方法有许多种,比如应用 阻塞队列 、或者是 生产消费者模型 等。不同的技术选择适用于不同的应用场景,大家应该根据具体的应用场景进行适当选择!
以上属于个人观点,欢迎大家指正交流,一起进步!