前面已经把mysql 和 jvm相关知识做了一遍复习与梳理。这一章开始就开始java并发编程的知识记录与讲解,并发编程相对前面的内容,会更复杂,更难一些。我也梳理了很久的一个顺序以及需要说明的内容。不过没关系,付出总是有回报的,学习能够让自己更加强大。希望在学习完整个java并发编程后,能在工作和学习中有所帮助。
这一篇理论的地方特别多,需要理解的地方也是很多,所以需要静下心来好好的读一读,缕一缕这些逻辑,一个点没有明白就需要多看两遍。
我们老是听到并发,高并发等关键词,面试和工作中也是比较频发各种各样的并发问题处理,有些问题还特别奇怪(本人也比较烦去梳理并发问题原因),因为并发问题牵扯到太多的情况,比如内存共享,执行顺序,冲突等等等。。。。都需要一一去验证。那么我们就从原理上面去分析这些情况的发生是为什么
首先我们要了解什么是并发,并发的概念是什么
众所周知计算机是通过cpu来执行各种各样的指令来解决我们的问题,运行我们的程序,最早的cpu是单核,慢慢的演变到现在的多核多线程,那么这么多的核的作用是啥? 最明显的就是快
并行 : 指在同一时刻,多个任务或指令在多个处理器下共同执行
并发:同一时刻只有一个指令在处理器下执行,多个指令在cpu下被快速的轮换执行,使得给我们的感觉是多个指令在同时执行,但是实际上只是并不是同时执行,只是分成若干个时间片,多个线程快速交替执行
区别:
它们都是为了最大化cpu的利用产生的
并行在多处理器下存在,并发可以在单处理器和多处理器下都存在,并发是并行的假象,更像是模仿并行,可以理解为一个天才(并行)两个手一起写字,能写两份文件,还有一个东施效颦的(并发)也想这样,但是左手不会写,一个手在两份文件上交替的去写文件。
一个手两边抓,肯定会有问题。万一你写着写着忘了写哪儿了呢,一边英文一边中文,结果写反了呢,等等问题出现。
三大特性也是并发bug的三大源头,解决了这三个问题,基本都能解决并非的bug(绝大部分)
可见性:当一个线程对变量进行了修改,别的线程也能看到这个修改
有序性:程序按代码的顺序执行,jvm会指令重排(jvm的优化)所以会有顺序问题
原子性:一个或者多个操作,要么都成功,要么都失败
下面做一下简单的示例,看一下以上三个问题:
可见性问题:
package com.demo;
/**
* 可见性问题
*/
public class VisibilityProblem {
private boolean flag = false;
public void refresh(){
flag = true;
System.out.println("修改flag的值为:"+flag);
}
public void run(){
System.out.println("开始执行");
int i = 0;
while(!flag){
i++;
}
System.out.println("跳出循环"+i);
}
public static void main(String[] args) throws InterruptedException {
VisibilityProblem vp = new VisibilityProblem();
//开始执行run的死循环, flag作为循环条件
Thread t1 = new Thread(() -> vp.run());
t1.start();
//休眠一秒然后在去修改flag的值
Thread.sleep(1000);
Thread t2 = new Thread(() -> vp.refresh());
t2.start();
}
}
运行结果如下:
明明线程2去修改了flag的值,但是还是没有跳出循环,对于线程1而言,并没有看见线程2做的修改,出现可见性问题
有序性问题:
public class ReOrderTest {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
while (true) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
//两个线程交替该值,按逻辑来说这个循环永远不会退出,
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
thread1.start();
thread2.start();
thread1.join();//线程1执行完才允许其他线程执行
thread2.join();
System.out.println("第" + i + "次(" + x + "," + y + ")");
//当x y的值都为0时退出
if (x == 0 && y == 0) {
break;
}
}
}
}
运行结果:
可以看到,如果按照代码逻辑,线程2在获取a的值时应该已经被线程1所修改为a = 1了,所以y的值应该永远不为0,可是在多次运行后,总是能退出,x y都同时为0,这就是线程有序性问题,x与y大部分是0,1但还是有一些因为指令重排的问题变成了1,0(自行测试就能看到,而且可以尝试把线程去掉,只做赋值就形成了我们按代码逻辑推算出来的永远不会退出)
原子性问题:
最常见的就是i++ 大家都知道i++不保证原子性,就是说你循环10w次i++ ,算出来不一定是10w,可能会少很多
以上三个就是并发问题得三大特点了。可以思考下为啥会出现这些问题?解决方案我在上面也已经写出来了,当然后续我们把原理弄清楚,解决起来就得心应手了
java虚拟机规定了java得内存模型jmm (Java Memory Model,Java内存模型),用来屏蔽掉在各个操作系统和硬件之间得差异,达到一致得并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开的。
看到这个图,其实我们可见性问题得原因就能清楚了,线程1获取了变量的值,在线程2改变了以后虽然主内存中的变量是改变了,但是对于线程1还是使用的本地内存的变量副本,导致跳不出循环