预备知识:
什么是线程?
线程:当一个方法被调入栈,执行完出栈的过程就是一个线程
栈和队列都是由数组实现的,因为数组对于内存的消耗小
什么是进程?
凡是带有主方法,依赖自己的主方法运行起来的程序就是进程,进程是由线程组成的,句柄就是变量
进程和线程有什么不同?
进程是由线程组成的,进程有的功能线程都有,但是线程有的功能进程不一定有。例如,进程之间的通信方式线程都有,但是线程之间的通信方式进程不一定有。
高速缓存存在的意义:
为了支撑多核cpu而诞生的,高度缓存的容量越大,所支撑的核心越多,存储量越大,支撑的核心越多(不用频繁的往内存中读写数据了,减少与内存的交互)
内存:被称为闪存(断电后数据就没有了)
硬盘:永久存储/持久化存储
cpu存在的问题:CPU运算速度快,从硬盘直接读取数据到cpu太慢,导致cup大多数时间都在空转。CPU利用率太低
内存的容量大,CPU从内存读取速度快,可以将数据先加载到内存中,然后CPU计算时从内存加载数据。
内存存在的意义:缓和CPU计算速度快和从硬盘读取数据慢之间速度不匹配的问题。
多级缓存的作用: 缓存的基本目的是减少访问数据的延迟。在多用户或高并发环境下,多级缓存可以通过减少对共享资源的争用。
多级传输的作用:加快传输
当进程进入就绪队列中,先进入的先执行的概率大,不一定是先进入的先执行
并行:多个线程各自执行各自的资源
并发:多个线程竞争相同的资源
正文
为什么要有并发编程?
从工程的角度来说,一个技术的产生对应着他要解决的问题,我们要说为什么产生并发编程,首先要说并发产生的问题。
计算机刚出现的时候,应该都是单核单线程,就是来了一个任务创建一个线程去执行该任务,听起来很正常对吧,然后我们再来细说一下,计算机是如何执行一个任务的。
1.cpu接到指令去执行一段代码,代码需要读取一部分磁盘数据
2.cpu就向DMA下达读取磁盘上数据的指令,下达完指令之后cpu就等待着了
3.DMA发送给磁盘需要读取的指令,然后由磁盘加载到内存中,并反馈给DMA
4.DMA收到反馈后,再反馈给CPU,告诉CPU去内存中读取数据
5.CPU去内存读取数据,运行代码
如图
这时候就产生了一个问题:
在磁盘把数据加载到内存的时候,CPU一直是空闲的,而且加载数据的过程中不需要CPU参与,这就造成了cpu资源的浪费。
为了解决这个问题,就提出了多线程,就是在读取数据的过程中,去让cpu执行别的任务,读取完之后cpu再来执行代码。多线程就由此而生了。
那么多线程一定比单线程快吗?
我们执行一段代码来看一下
public class ConcurrencyTest {
private static final long count = 1000000000l;
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
thread.join();
System.out.println("concurrency :" + time+"ms,b="+b);
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial:" + time+"ms,b="+b+",a="+a);
}
}
分别验证1w~1000w时单线程和多线程的比较
1w时
10w
100w
1000w
10亿
但是cpu切换也需要时间。单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。到这里我们就会发现如果频繁的切换线程也会浪费很多时间,就也会造成资源浪费问题。这个我们要分情况来讨论,
如果时cpu密集型作业-------那么就是单核单线程块
假设我们有一段需要cpu运算10s的代码,这时候如果是单线程执行,完成任务也就是10s,如果是在单核多线程的的情况下,我们就需要频繁的切换上下文,那么完成任务的时间就会超过10s。
如果是I/O操作密集型作业------那么就是单核多线程快
假设我们有一段需要cpu计算3s,I/O写入7s的操作,当单线程的情况下就会cpu运行完3s后,等待7s,等完成这段代码后去执行下一段代码,cpu的利用率30%,但是如果在多线程的情况下,cpu就能完成3s的计算后,切换下一个进程,就会提高cpu 的利用率。
减少上下文切换的方法
无锁并发编程、CAS算法、使用最少线程和使用协程。
·无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
·CAS算法:(比较并交换)
CAS:就是如果有两个进程去访问内存,想要修改内存中的数据,两个线程首先会复制一份该内存中的数据到缓存中,然后其中线程A对数据进行修改,并保留原有数据,然后用缓存中的原数据和内存中的数据进行对比,要是一样就把缓存中修改的数据写入内存中,这时候后如果另一个线程B也想修改数据,就会在缓存中写入修改的数据并保留原数据,这时候线程B把修改数据写入缓存的时候会先去对比缓存中的原数据和现在内存中的数据,因为内存中数据已经经过修改,所以就会和缓存中的原数据不一致就无法修改。
Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
·使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
·协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
并发编程锁的概念:
在多核并发编程的情况下会出现不一致的情况我们举个例子,假设有两个线程A和B同时对一个数据进行修改,线程A和线程B都取到数据,A对数据+100,然后写回内存,B对数据+200,然后在写回数据最后数据就是原数据+200,就出现了数据不一致的问题,这时候就出现了一个东西叫做锁,来解决这个问题。
锁的意义就是:在同一时间内,只允许一个线程获取此资源
当加锁之后,上述例子就不会出现两个线程同时获取一个资源的情况了就会避免不一致的问题。