并发编程就是充分利用多核CPU的计算能力,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
他们2个的目标都是最大化CPU的使用率。
并发:
同一时刻,多个任务在同一个CPU核上, 只能有一条指令在执行,但是多个线程指令快速的轮动交替执行,给人感觉任务是同时执行的。
并行:
同一时刻,多个处理器或多核处理器同时处理多个任务,是真正意义上的同时进行。
串行:
有N个任务,由一个线程按顺序执行,由于任务、方法都在一个线程执行,所以不存在线程安全问题。
打个比方
并发=2个队列和一台咖啡机。
并行=2个队列和二台咖啡机。
串行=1个队列和一台咖啡机。
我们知道并发编程为了保证数据的安全,必须满足以下三个特性:
原子性,指的是在一个操作中CPU 不可以在中途暂停然后再调度,要么不执行,要么就执行完成。
可见性,指的是多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改后的值。
有序性,指的是程序执行的顺序按照代码的先后顺序执行,而不能随便重排,导致程序出现不一致的结果。
总结下来就是:并发编程会带来原子性问题、可见性问题、有序性问题
缓存一致性问题其实就是可见性问题;
处理器优化是可以导致原子性问题的;
指令重排即会导致有序性问题;
1、要知道计算机在执行程序员的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道,而计算机上的数据,都是存放在计算机的物理内存上的。
2、当内存的读取速度和CPU的执行速度相比差别不大的时候,这样的机制是没有任何问题的,可是随着CPU技术发展,CPU的执行速度和内存的读取速度,差距越来越大,导致CPU的每次操作内存都要耗费很多时间等待。
3、所以在CPU在物理内存上增加了高速缓存,这样程序执行过程也发生了改变,变成了程序在运行过程中,会将运算所需要的数据从主内存复制到一份到CPU的高速缓存中,当CPU进行计算时就直接从高速缓存中读取数据和和写数据了,当运算结束时再将数据刷新到主内存就可以了。
4、但是当CPU出现多核概念后,每个核都有自己的一套缓存并且还可以支持多线程,最终演变成多个线程访问进程中的某个共享内存,且多个线程在不同的核心上,则每个核心都会在各自的缓存中保留一份共享 内存的缓冲,因为多核是可以并行的,这样就会出现多个线程各自写各自的缓存,导致各自的缓存内容出现不一致。
注意:正是因为这个现象,所以造成了并发编程时出现可见性的问题。
为了使处理器内部的运算单元能够被充分利用,处理器可能会对程序代码进行乱序的执行,这就是处理器优化。
除了主流的的处理器会对代码进行乱序处理,很多编程语言也会有类似的变化,如java虚拟机的即时编译器(JIT)也会做指令重排。
可想而知,如果任由处理器优化和编译器的指令重排,就可能会在并发编程实际开发中,出现各种各样的问题。
如:原子性问题和有序性问题。
1、java为了保证并发编程中可以满足原子性、可见性、有序性、于是诞生了一个重要的概念,那就是java内存模型(注意,他和JVM内存模型不是一个东西),java内存模型定义了共享内存系统中多线程多谢操作的行为规范。
2、通过这些规则来对内存的多写操作,从而保证指令的正确性,它解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性、有序性。
简单总结就是,java 内存模型定义了共享系统中多线程中多谢操作的行为规范,从而解决并发编程的问题而存在。
主要存储的是java对象实例,所有线程创建的线程都存放在主内存中,不管是实例对象是成员变量还是方法中的局部变量,当然也包括共享类的信息、常量、静态变量。由于这是共享数据区域,多条线程对同一变量进行访问访问就可能会发生线程安全问题。
主要储存当前方法的所有本地变量信息(工作内存中储存着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程都是不可见的,所以无法互相访问工作内存,因此存储在工作内存的数据是不存在安全问题的。
假设主内存中存在一个共享变量X,现在A和B线程2个线程分别对该变量赋值,接下来A对x进行赋值为2,而B线程想要读取x的值,那么这时候B线程读取到的值到底是A线程更新后的值还是原本主内的值呢?
答案是不确定的,即有可能读取到A更新的值,也有可能读取的更新之前的值。
原因
是因为每个线程的工作内存是私有的,如线程A改变x值时,首先是将变量从主内存COPY到A线程的工作内存中,然后对变量进行操作,操作完成后,再将变量x写回主内存,而B线程也是类似的情况,这样就有可能造成主内存和工作内存的数据存在一致性问题。
假设1:
A线程修改完后正要将数据写回主内存,而B线程此时正在读取主内存,即将x=1,COPY到自己B线程的工作空间,这样B线程就读取到的x值=1.
假设2:
但是如果A线程已经将结果写回了主内存,这时候B才开始读取的话,拿到的值就是x=2。
1、lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态。
2、read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
3、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
4、use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
5、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值,赋给工作内存的变量
6、store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
7、write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
8、unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
指的是在一个操作中CPU 不可以在中途暂停然后再调度,要么不执行,要么就执行完成。
原子性案例:
对基础属性赋值属于原子操作
X=10; //原子性(简单的读取、将数字赋值给变量)
以下均不属于原子操作,在没有处理的情况下,在多线程环境均不安全
Y = x; //变量之间的相互赋值,不是原子操作
X++; //对变量进行计算操作
X = x+1;
User user = new User();
java里如何解决原子性问题?
通过 synchronized 关键字保证原子性。
通过 Lock保证原子性。
通过 CAS保证原子性。
指的是多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改后的值。
java如何保证可见性
通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized 关键字保证可见性。
通过 Lock保证可见性。
通过 final 关键字保证可见性
可见性案例:
可以看到2个线程都执行了,但是B线程修改了flag的值为true,但是A线程依旧无法感知到。
/**
* @author ljc
*
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
*/
public class VisibilityTest {
private boolean flag = true;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag");
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
int i = 0;
while (flag) {
i++;
//TODO 业务逻辑
}
System.out.println(Thread.currentThread().getName() + "跳出循环: i=" + i);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB通过flag控制threadA的执行时间
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
}
输出结果:
threadA开始执行.....
threadB修改flag
指的是程序执行的顺序按照代码的先后顺序执行,而不能随便重排,导致程序出现不一致的结果。
一条线程串行执行是不会出现有序性问题,但是如果是多线程环境就可能会出现。
java如何保证有序性?
通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized关键字保证有序性。
通过 Lock保证有序性。
总的来说JMM就是为了解决三大特性、原子性、有序性、可见性的;