二:并发三大特性
JMM
由来
JMM(Java Memory Model)。不同硬件和不同操作系统在内存上是存在差异的,java为了解决相同代码在不同平台上出现的差异或者问题,使用JMM-Java内存模型来解决这些差异。使并发编程能够跨平台。
Java内存模型是一种抽象的概念,并不真实存在,定义了Java程序在各种平台下对内存访问的机制及规范。
介绍
JMM规定所有的变量都会存储在主内存中。在操作的时候,需要从主内存中复制一份到线程内存中(CPU内存),在线程内部做计算,最后写回主内存中(不一定能写进去)。
在JMM中,所有的共享变量都存储在主内存中,每个线程都是将主内存中的共享变量拷贝一份在副本中,到线程的内存中,修改后,同步到主内存中。
线程之间的共享变量存储在主内存(main memory)中
每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
JMM原子操作图
具体流程解释:
lock (锁定) : 将主内存变量加锁,表示为线程独占状态,可以被线程进行read
read(读取) :线程从主内存读取数据
load(载入):将上一步线程从主内存中读取的数据,加载到工作内存中
use(使用):从工作内存中读取数据来进行我们所需要的逻辑计算
assign(复制):将计算后的数据赋值到工作内存中
store(存储):将工作内存的数据准备写入主内存
write(写入):将store过去的变量正式写入主内存
unlock(解锁):将主内存的变量解锁,解锁后其他线程可以锁定该变量
原子性
什么是原子性
一个原子性的操作是不可分割、不可被中断的,要不执行成功,要不全部失败。
原子性代码测试(i++)
package com.xqm.juc.threeCharacter;
import java.util.concurrent.CountDownLatch;
public class Test03 {
private static int count=0;
private static void incre(){
try {
Thread.sleep(10);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 200; i++) {
incre();
}
});
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 200; i++) {
incre();
}
});
thread.start();
thread1.start();
try {
thread.join();
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 发现每次执行的结果不一样,因为不是原子性的
System.out.println(count);
}
}
package com.xqm.juc.threeCharacter;
import java.util.concurrent.CountDownLatch;
public class Test02 {
private static int count=0;
private static CountDownLatch countDownLatch=new CountDownLatch(200);
private static void incre(){
try {
Thread.sleep(10);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
new Thread(() -> {
incre();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 发现每次执行的结果不一样,因为不是原子性的
System.out.println(count);
}
}
怎么保证原子性
synchronized、CAS、lock锁、ThreadLocal
synchronized
i++操作不是原子性,操作时分为三个步骤:
1.从主内存中获取数据保存在CPU的寄存器中
2.在寄存器中进行+1操作
3.将结果写回主内存中
package com.xqm.juc.threeCharacter;
/**
*synchronized
*/
public class Test04 {
private static int count=0;
private static void incre(){
// 加锁,实现
synchronized (Test04.class){
count++;
}
}
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 200; i++) {
incre();
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回结果都是200
System.out.println(count);
}
}
CAS
compare and swap 。比较和交换,是一条CPU的并发原语,采用的是乐观锁。不是java层面的。
因为是CPU级别的,因此效率比synchronized要高,synchronized是内核级别的。
过程:在修改属性之前,获取属性的值和地址。在本地Thread内存修改完属性的值后,要写入主内存,这时候再次获取原地址上的值,与先前获取的值进行比较,看是否有修改(比较操作),如果没有修改的话,那么就将新值写入主内存(替换操作)。
java中基于Unsafe的类提供了对CAS的操作方法,JVM会帮助实现CAS的汇编指令。
但是:CAS只实现了比较和交换,在获取原值的操作,需要自己去实现。
package com.xqm.juc.threeCharacter;
import java.util.concurrent.atomic.AtomicInteger;
/**
*synchronized
*/
public class Test05 {
private static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 200; i++) {
count.incrementAndGet();
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回结果都是200
System.out.println(count);
}
}
CAS问题
1.CAS只能保证对一个变量的操作是原子性,无法对多行代码实现原子性
2.CAS可能出现ABA问题(使用版本号来解决),使用AtomicStampedReference,比较版本和原值,用来解决ABA问题
package com.xqm.juc.threeCharacter;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test06 {
public static void main(String[] args) {
AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA", 1);
// 原来的值是什么
String oldValue = reference.getReference();
// 版本号
int oldVersion = reference.getStamp();
// 原值-新值-原版本号-新版本号
boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
// 修改失败,因为2版本的oldValue是B。可是这里是AAA,返回false
boolean b1 = reference.compareAndSet(oldValue, "B", 2, 3);
System.out.println(b);
System.out.println(b1);
}
}
CAS自旋时间过长问题:
1.可以指定CAS一共循环多少次,如果超过次数,直接判断失败。(自旋锁、自适应自旋锁)
2.可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回
lock锁
涉及并发比较多时,ReentrantLock比较好,因为synchronized有锁升级。
package com.xqm.juc.threeCharacter;
import java.util.concurrent.locks.ReentrantLock;
/**
* synchronized
*/
public class Test07 {
private static int count = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void incre() {
// 加锁,实现
lock.lock();
try {
count++;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 200; i++) {
incre();
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回结果都是200
System.out.println(count);
}
}
ThreadLocal
作用:线程隔离
不操作临界资源,
package com.xqm.juc.threeCharacter;
/**
* ThreadLocal
*/
public class Test08 {
static ThreadLocal t1=new ThreadLocal();
static ThreadLocal t2=new ThreadLocal();
public static void main(String[] args) {
// 主线程用来存
t1.set("aaa");
t2.set("bbb");
//线程获取不到其他线程的数据
new Thread(()->{
// 输入null
System.out.println(t1.get());
// 输出null
System.out.println(t2.get());
}).start();
// aaa
System.out.println(t1);
// bbb
System.out.println(t2);
}
}
ThreadLocal详细解析
- 每个Thread中都存着一个成员变量,ThreadLocalMap
// Thread类中
ThreadLocal.ThreadLocalMap threadLocals = null;
- ThreadLocal本身不存数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap。
- ThreadLocalMap是基于Entry[]实现的,因为一个线程可能有很多threadLocal,因此底层使用数组存储一个一个的ThreadLocal。
- 每个线程都有自己的ThreadLocalMap,Entry[]数组中k是ThreadLocal,v是线程的值
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// entry数组中,key是当前ThreadLocal,value就是线程的值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
- ThreadLocalMap的key是一个弱引用,在GC时会被回收
ThreadLocal的内存泄露问题:
如果ThreadLocal引用丢失,key因为弱引用会被GC回收,同时线程没被回收,就会导致value无法被回收和获取,导致内存泄露。
解决方法:在使用完ThreadLocal对象后,即使调用remove方法,移除entry就可以。
可见性
什么是可见性
可见性问题是出现在CPU中,CPU处理数据太快了,相对于CPU来说,从内存中取数据就很慢 ,因此cpu就提供了L1、L2、L3三级缓存,每次从内存中拿取数据,就会存储到三级缓存,取数据的速度就大大加快。
现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,这就导致每个线程中修改数据只会修改自己的工作内存,没有及时同步到主内存,导致数据不一致的问题。
可见性测试:
package com.xqm.juc.threeCharacter;
/**
* 线程可见性问题
*/
public class Test09 {
private static boolean flag=true;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (flag) {
// ...
}
System.out.println("Thread1线程结束");
}, "Thread1");
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里的修改不能即使放到主内存中,导致线程会一直死循环
flag=false;
System.out.println("主线程将flag进行修改");
}
}
怎么保证可见性
volatile、synchronized、final、lock
volatile
volatile是一个java关键字,能够保证线程可见性和禁止指令重排序的问题。
被volatile修饰的变量,会被告诉CPU,对当前变量的操作,不允许使用三级缓存,必须去主内存操作,每次修改完会立刻写到主内存中。
volatile的内存语义
- volatile变量被写:一旦被修改,JMM会将当前线程对应的CPU缓存立刻刷新到主内存中
- volatile变量被读:读一个被volatile修饰的变量时,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量
其实加了volatile修饰符,就是告诉CPU禁止使用三级缓存,变量在被汇编的时候,会追加一个lock前缀,CPU执行指令时,如果带有lock前缀,CPU会做两件事:一是将当前处理器缓存行的数据写回到主内存;二是写回的数据,在其他CPU缓存中是无效的。
package com.xqm.juc.threeCharacter;
/**
* 线程可见性问题
*/
public class Test09 {
// 加上volatile就可以解决可见性问题
private volatile static boolean flag=true;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (flag) {
// ...
}
System.out.println("Thread1线程结束");
}, "Thread1");
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里的修改不能即使放到主内存中,导致线程会一直死循环
flag=false;
System.out.println("主线程将flag进行修改");
}
}
synchronized
synchronized除了解决原子性的问题,还能解决可见性的问题。使用的是synchronized内存语义。
如果涉及到synchronized的同步代码块或同步方法,获取到锁资源后,内部涉及到的变量从CPU缓存中移除,从主内存中拿取数据。在释放锁资源后,会立即将缓存中的数据同步到主内存中。
package com.xqm.juc.threeCharacter;
/**
* 线程可见性问题,使用synchronized
*/
public class Test10 {
private static boolean flag=true;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (flag) {
// 在拿到锁的一瞬间,会重新取得flag
synchronized (Test10.class){
// ...
}
}
System.out.println("Thread1线程结束");
}, "Thread1");
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=false;
System.out.println("主线程将flag进行修改");
}
}
final
final和volatile不能同时修饰一个属性。
被final修饰的
lock
lock锁也能保证线程可见性,但是实现方式和synchronized不同,synchronized是基于内存语义,而lock锁是基于volatile实现的。
lock锁内部在获得锁和释放锁时,会对一个有volatile修饰的state变量进行加一和减一 的操作
package com.xqm.juc.threeCharacter;
import java.util.concurrent.locks.ReentrantLock;
/**
* 线程可见性问题,使用synchronized
*/
public class Test11 {
private static boolean flag=true;
private static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (flag) {
lock.lock();
try {
// 业务代码
}finally {
lock.unlock();
}
}
System.out.println("Thread1线程结束");
}, "Thread1");
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里的修改不能即使放到主内存中,导致线程会一直死循环
flag=false;
System.out.println("主线程将flag进行修改");
}
}
有序性
什么是有序性
.java文件会被编译、加载之后,交由机器来执行。在执行引擎中,为了提升执行效率,在不影响结果的前提下,会对指令进行重排序,因此java的程序时乱序的。
多线程执行乱序的指令会出现问题。
验证有序性:x和y会出现等于0的情况
package com.xqm.juc.threeCharacter;
/**
*
*/
public class Test13 {
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 thread = new Thread(() -> {
a = 1;
x = b;
});
Thread thread1 = new Thread(() -> {
b = 1;
y = a;
});
thread.start();
thread1.start();
try {
thread.join();
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (x==0 && y==0){
System.out.println("第"+i+"次出现x=0 & y=0");
}
}
}
}
第1707159次出现x=0 & y=0
第1719422次出现x=0 & y=0
第1726500次出现x=0 & y=0
DCL(double check lock)来解决创建单例时的有序性问题:
package com.xqm.juc.threeCharacter;
/**
* DCL double check lock
*/
public class Test14 {
private static Test14 instance;
public static Test14 getInstance(){
if (instance==null){
synchronized (Test14.class){
if (instance==null){
// 开辟空间--初始化--指针链接地址
instance=new Test14();
}
}
}
return instance;
}
}
怎么保证有序性
as-if-serial、happens-before、volatile
as-if-serial
as-if-serial语义:需要保证单线程的程序执行结果是不变的,如果存在依赖的关系,也不可以做指令重排。
// 这种情况就不允许做指令重排
int i=0;
i++;
编译器、runtime和处理器都必须遵守as-if-serial语义
happen-before
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操
- happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
JMM遵守hassen-before规则来保证可见性问题。
JMM只有不出现上述八种规则的情况下,才不会触发指令重排的效果
volatile
使用volatile修饰属性,能够禁止指令重排序
内存屏障:可以将内存屏障看成一条指令