什么是并发编程?
首先我们要知道什么是并发? 什么是并行?
并行: 多件事情在同一时刻同时发生
并发: 在同一时间内,多个事情交替执行
并发编程: 比如抢票,秒杀等在同一场景下,有大量的请求访问同一资源, 会出现一些安全性的问题,所以要通过编程来控制多个线程依次访问资源,称为并发编程
引发并发编程的根本原因
因为所有的java代码都是在java虚拟机中运行的, 而java虚拟机也有自己的模型-----
Java 内存模型(Java Memory Model,JMM)
Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,在工作内存中操作完之后,再将数据写入主内存中.
这里的工作内存是 JMM 的一个抽象概念,也叫本地内存,其存储了该线程读 /写共享变量的副本, 就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。
并发编程的三个核心问题(不可见性,乱序性,非原子性)
1.不可见性
多个线程同时对共享资源进行操作, 彼此之间是不可见的,操作完之后再写入到主内存中,这样可能会出现问题,与我们预期的结果不相符
例如:
我们预期的结果应该是count=2;
然而这是不一定的, 当线程1从主内存中拿到count进行++操作后变成了1,还没来得及写入主内存时,线程2也拿到了count=0的数据进行了++操作是的其变成了1,两个线程写入主内存后最终的结果count是为1的,而不是2,也就是线程1操作了主内存中的数据,线程2并不知道.
2.无序性
为了优化性能,对一些代码指令进行了顺序重排,提高程序的速度
3.原子性
原子的意思代表着——“不可分”, 原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作, 线程切换导致了原子性问题
CPU 能保证的原子操作是 CPU 指令级别的(i++,就只有一条语句,进行一次操作),而不是高级语言的操作符(i++,有三个操作过程), 高级语言里一条语句往往需要多条 CPU 指令完成。如 count++,至少需要三条 CPU指令。
指令 1:首先,需要把变量 count 从内存加载到工作内存;
指令 2:之后,在工作内存执行 +1 操作;
指令 3:最后,将结果写入内存;
如何解决这三个问题?
1.使用volitile关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后:
1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2. 禁止进行指令重排序。
3. volatile 不能保证对变量操作的原子性。
volitile的底层实现原理
使用 Memory Barrier(内存屏障)。
内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其后面的指令移到内存屏障指令之前。
Volatile 变量编译为汇编指令会多出Lock 前缀.
有序性实现:主要通过对 volatile 修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性的。
可见性实现:主要是通过 Lock 前缀指令 + MESI 缓存一致性协议来实现的。对 volatiile 修饰的变量执行写操作时,JVM 会发送一个 Lock 前缀指令给CPU,CPU 在执行完写操作后,会立即将新值刷新到存,同时因为 MESI 缓存一致性协议,其他各个 CPU 都会对总线嗅探,看自己本地缓存中的数据是否被别人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个 CPU里的线程在读取改变量时,就会从主内存里加载最新的值了,这样就保证了可见性
可见性实现的例子:
public class ThreadDemo implements Runnable{/*volatile 修饰的变量,在一个线程中被修改后,对其它线程立即可见禁止cpu对指令重排序*/private boolean flag = false;//共享数据public void run() {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}this.flag = true;//让一个线程修改共享变量值System.out.println(this.flag);}public boolean getFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}
}
public class TestVolatile {public static void main(String[] args) {//创建线程任务ThreadDemo td = new ThreadDemo();Thread t = new Thread(td);//创建线程t.start();//main线程中也需要使用flag变量while(true){if(td.getFlag()){//false-trueSystem.out.println("main---------------");break;}}}
}
上面例子中,main主线程中执行一个子线程,还要执行一个while循环,只有当子线程中的flag为true时才可以跳出while循环
当共享数据flag没有被volitile修饰时,输出结果如下:
一直在死循环,没有跳出while循环,但是在子线程中flag已经改为了true,也已经输出了,说明main主线程和子线程彼此的操作时不可见性的, 但是有volitile修饰时,就会跳出循环.
有序性实现的例子:
/*模拟指令重排序*/
public class Reorder {private volatile static int x;private volatile static int y;private volatile static int a;private volatile static int b;public static void main(String[] args) throws InterruptedException {int i = 0;for(;;) {i++;x = 0; y = 0;a = 0; b = 0;Thread one = new Thread(new Runnable() {public void run() {a = 1;x = b;}});Thread other = new Thread(new Runnable() {public void run() {b = 1;y = a;}});one.start();other.start();one.join();other.join();String result = "第" + i + "次 (" + x + "," + y + ")";if(x == 0 && y == 0) {System.err.println(result);break;} else {System.out.println(result);}}}}
有volitile修饰时输出结果如下:
没有volitile修饰时:
可以看到,有volitile修饰时,代码的输出是有序的.
2.通过加锁的方式,让线程互斥执行来保证一次只有一个线程对共享资源访问,从而保证原子性.
synchronized : 关键字 修饰代码块,方法 自动获取锁,自动释放锁
ReentrantLock : 类 只能对某段代码修饰 需要手动加锁,手动释放锁.
3.在java中还提供一些原子类,在低并发情况下使用,是一种无锁实现.
原子类的原子性是通过 volatile + CAS 实现原子操作的。
AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value的内存可见性,这为后续的 CAS 实现提供了基础。低并发情况下:使用 AtomicInteger。
4.采用CAS机制(CAS(Compare-And-Swap) 比较并交换)从而保证原子性
CAS是实现乐观锁的一种方式,是一种自旋式思想,是一种轻量级的锁机制
CAS 包含了三个操作数:
①内存值 V
②预估值 A (比较时,从内存中再次读到的值)
③更新值 B (更新后的值)
当且仅当预期值 A==V,将内存值 V=B,否则什么都不做。
这种做法的效率高于加锁,当判断不成功不能更新值时,不会阻塞,继续获得 cpu执行权,继续判断执行。
自旋思想:
第一次从主内存中获取值到工作内存中,存储作为预期值,然后将对象数据修改,将工作内存中值写入到主内存, 在写入之前需要做一个判断.用预期值与主内存中的值进行比较,如果预期值与主内存中值一致,说明没有其他线程修改, 将更新数的值,写入到主内存,如果预期值与主内存中值不一致, 说明其他线程修改了主内存的值, 这时就需要重复操作整个过程
特点:
不加锁,所有的线程都可以对共享数据操作, 适合地并发是使用,由于不加锁,其他线程不需要阻塞,效率高,但是在大并发时,不停自旋判断,导致cpu占用率高,容易导致 CPU 跑满.
ABA 问题
当某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个线程使用预期值去判断时,预期值与内存值相同,当前线程的 CAS 操作无法分辨当前 V 值是否发生过变化。
如何解决ABA问题?
解决 ABA 问题的主要方式,通过使用类添加版本号,来避免 ABA 问题。如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了