并发:多个线程(进程)竞争一个资源
并行:多个线程(进程)同时运行不同资源
线程和进程的关系简单地说,进程是一个容器,一个进程中可以容纳若干个线程,一个进程里面,至少有一个线程
跨系统的并发实现底层原理需要更好的机器来实现,不是跨系统的就是调用**操作系统的CAH**来实现,如下图所示
线程的状态
- 新建--就绪--运行--阻塞--等待---死亡
- 就绪队列、阻塞队列、等待队列
- 一个线程申请资源时,无法让其马上执行,而是先进入队列变为就绪态,然后等待资源分配,若马上就让其直接执行,会导致之前正在执行的进程中断、丢失
- 和操作人员交互多的指令,在运行时优先级越高、响应也越迅速
- 每个进程在CPU中分配的时间片是不同的,当该进程的时间片用完且该进程还没完成时就会中心进入就绪态,等待下一次执行,直到完成为止,同时,若一个进程在分配的时间还没用完就已经完成,此时CPU会马上切换任务,不会休息(比如进程A共需要5ms,但是cpu分配了10,此时在执行到5ms时,CPU便不再处理A,转而执行B),另外,只有没有任何进程时,CPU才会休息
- 不同的系统,每次为进程分配的时间片方式不同,一般是随机的,也有等长的,比如linux
- 在操作系统中,每个进程是否可以进入就绪、运行是靠优先级决定的,具体如何计算优先级每个系统的方法可能不一样,主要方法有优先级队列、等待响应比等
- 当一个进程的时间片用完,操作系统会记录此时的状态(即执行到哪里,也就是执行到的地址)和下一次执行时间,以便下一轮时间片到的时候可以继续执行
- 操作系统选中哪个线程去执行是不确定的,是随机的
- 进程执行完后进入死亡状态,时间片用完重新回到就绪态
- 阻塞队列:竞争枷锁资源失败的时候进程A就会进入阻塞队列,若某一刻被竞争的资源被正在占用的进程B释放,此时A又可以进去就绪队列竞争资源
- 等待队列:一个处于等待队列的进程A是不会自己接触等待状态的,只有当别人唤醒时,他才能进入就绪态去竞争资源,同时一个进程可以自己进入等待队列,正在执行的进程也可以自己进入等待队列
- 先进入就绪态的被先执行的概率越大,但不是百分之百,后进入就绪态的可能比先进入的更早执行
- 在线程完定义的变量,若需要出现在线程内,则该线程会默认的将该变量认为是final修饰的,此时若该变量是基本数据类型,则不能重新赋值,若是引用变量,则指向不能改变,但是内容可以改变
int a=10;
int[] arr = {0};
Thread x1 = new Thread(){a=100; //错误@Overridepublic void run(){arr[0] = 100; //不报错,因为指向没有变}
};
在有多个线程时,main线程会最先执行,然后是剩下的线程平等的竞争CPU,每个线程之间都是互不等待的,例如:
- 在上述代码中,arr[0]大概率是0,因为main先执行,当x1与x2进入就绪态时,分配给main的时间片可能还未用完,便会直接执行输出,但这是大概率,也有可能在main线程未执行到输出时时间片用完,此时arr[0]就会改变
- 若此时,我们加上join,就会取消掉互不等待,
此时,x1执行完之后,x2才会执行,最后是输出,注意,x1.join()执行,只是让其之后的等待,但x2是在x1.join()之前进入的就绪态,所以此时并不会让x2线程中的内容停止执行,这时候就是竞争CPU了
- 从严格的物理上讲,同一时刻只能有一个线程对变量进行操作,但是当我们有了告诉缓存后,只有在第一次对其执行的时候需要占用总线,剩下的只需要在缓存中操作即可,大大加快了速度与并发效率,但是,特也产生了并发问题
- 比如上述的两个线程对arr[0]进行+1操作,可能会有多种情况,但是最后arr[0]的结果只能是小于等于20000的,最小的结果是1
- 那么如何解决并发问题呢?
- 锁机制
- synchronized 重量级重入锁,
- 当synchronized锁静态方法时,锁的是该方法,如果别人想要调用,就要先加锁,且每次调用都要加锁,想调用,就加锁
上下文切换
**多线程一定比单线程快吗?**
- 不一定,因为有上下文切换
- 在产生CPU浪费的情况下,多线程合适,无浪费的情况下,单线程合适
- 在执行的操作次数越多,多线程就越快,次数越少,单线程相比更快,如下图:
原因:因为线程有创建和上下文切换的开销
当执行的操作次数很少时,CPU的使用率与多线程时相差不多,但是多线程会有一个创建和上下文切换的开销,所以总的时间就会变长;当执行操作次数很多时,单线程就会因为IO的时间而导致CPU在这期间会“发呆”(没有操作来使用CPU),导致CPU的使用率大大降低,而多线程在出现IO时并不会等待IO的结束,而是会直接执行其他操作,只要该操作的CPU使用时间完成,就直接让下一个操作来占用CPU,此时CPU就会不间断的工作起来,不再休息
- 线程性能测量工具
- 使用Lmbench3来测量上下文切换的时长
- 使用vmstat可以测量上下文切换的次数
- 如何减少上下文切换的次数
- 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一 些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
- CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁
- 使用最少线程数:避免创建不必要的线程。比如任务很少但是创建的线程很多
- 协程
- 减少上下文切换的实战
- 通过减少线上大量WAITING的线程,来减少上下文切换次数
- 第一步:用jstack命令dump线程信息(jsatck指令)
- 第二步:统计所有线程分别处于什么状态(grep指令)
- 第三步:打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么
- 第四步:减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降到 100
- 第五步:重启JBOSS,再dump线程信息,然后统计WAITING(onobjectmonitor)的线程(WAITING的线程少了,系统上下文切换的次数就会少,因为每一次从 WAITTING到RUNNABLE都会进行一次上下文的切换)