认识JMM
JMM 是指Java Mermory Model ,java 内存模型;它的目的是用来屏蔽计算机硬件和操作系统之间的差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM 规定所有的变量都存储在内存中(主内存/主存),每个线程有自己的工作内存;线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存中的数据。不同的线程之间也无法直接方队对方工作内存的变量,线程间变量值的传递均需要通过主内存来完成。
并发编程需要解决的三类问题:原子性问题,可见性问题,有序性问题
原子性
什么是原子性
并发编程中的原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。
验证原子性问题
下面我们通过一段java程序一起验证原子性问题
我们使用100个线程,每个线程进行1万次累加操作;如何多线程没有原子性问题,那么所有的线程执行完毕以后,应该n的值应该是100 0000。
private static long n = 0L;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
n++;
}
latch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
latch.await();
System.out.println(n);
}
实际结果并非是预期的100 0000,出现了原子性问题。
如何保证原子性
我们可以通过如下手段来保证原子性;
-
synchronized 保证原子性
-
CAS 操作保证原子性,即通过compare and sweep 来实现
例如:AtomicInteger 或者 Unsafe.getUnsafe().compareAndSwapXXX 操作
-
使用ReentrantLock 操作保证原子性
可见性
什么是可见性
之前描述了JMM,线程操作变量都是在自己的工作内存中操作的,并且线程也无法看到其他线程的工作内存中的变量;所以多线程提高效率,本地缓存数据,造成数据修改不可见,要想保证可见,要么触发同步指令,要么加上volatile,被修饰的内存,只要有修改,马上同步涉及到的每个线程。
验证可见性问题
下面我们通过一段java程序一起验证可见性问题
public class Test {
static boolean flag =true;
public static void main(String[] args) throws InterruptException{
Thread t1 = new Thread(()->{
while(flag){
}
System.out.println("循环结束");
});
t1.start();
Thread.sleep(100L);
flag = false;
System.out.println("主线程修改结束");
}
}
运行后,线程t1 没有像预期的那样停止,主线程对标志位的修改,对t1线程不可见。
如何保证可见性
知道多线程在并发环境中有可见性问题以后,我们该如何在工作中避免发生可见性问题呢?
我们可以利用如下手段,来保证可见性:
-
使用volatile 来保证可见性
为什么volatile可以保证可见性?
volatile的语义决定的;
当对volatile修饰的变量进行读操作时会将CPU缓存的变量副本设置为无效,必须去主内存重新读取。
当对volatile修饰的变量进行写操作时会将当前线程对应的CPU缓存及时的写入到主内存中去。 -
使用synchronized 来保证可见性
为什么synchronized可以保证可见性?
synchronized修饰的同步代码块或者同步方法,在获取锁资源后,会将内部涉及到的变量CPU缓存中移除,就必须重新去主内存中获取;而且在锁资源释放之后,会立即把CPU缓存中的数据立即同步到主内存中去。
-
使用ReentrantLock 来保证可见性
有序性
什么是有序性
我们编写的.java程序经过编译器编译后会生成.class文件,.class文件在实际成CPU能够识别的一条条的指令集;而CPU在执行这些指令的时候有可能时乱序执行的。
验证有序性问题
下面我们通过一段代码来验证有序性问题。经典面试题
public class Test {
static int a,b,x,y;
public static void main(String[] args ){
for(int i=0;i<Integer.MAX_VALUE; i++){
a =0;
b =0;
x =0;
y =0;
Thread t1 = new Thread(()->{
a=1;
x=a;
});
Thread t2 = new Thread(()->{
b=1;
y=b;
});
t1.start();
t2.start();
t1.join();
t2.join();
if(x==0&&y==0){
System.out.print("出现非预期结果");
break;
}
}
}
}
如何保证有序性
为了防止出现有序性问题,我们可以通过volatile 来保证有序性。
关于有序性,CPU的乱序执行其实是为了提高CPU的执行效率,从宏观层面看上去就像是as if serial 。
同时JMM为了保证有序性,也有相关的原则,我们称之为happen-before 原则;分别如下:
-
程序次序规则
在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后买的操作。
-
管程锁定规则
一个unlock操作先行发生于后面对同一个锁的lock操作,后面是指时间上的先后。
-
volatile 变量规则
对一个volatile变量的写操作线程发生于后面对于这个变量的读操作,后面是指时间上的先后。
-
线程启动规则
线程的start方法先行发生下对于此线程的每一个动作。
-
线程终止规则
线程中的所有操作先行发生于对此线程的终止检测。
-
线程中断规则
对线程interrupt方法的调用,先行发生于被中断线程代码检测到中断事件的发生。
-
对象回收规则
对象的初始化先行发生于对象的finalize方法的开始
-
传递性规则
A先行发生于B,B先行发生于C;那么A先行发生于C。
Mark
synchronized 可以保证原子性,可见性。
volatile 可以保证可见性,有序性。
- 本文链接: https://www.sunce.wang/archives/这一次肝了多线程与高并发之并发编程的特性
- 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!