五、JMM:Java内存模型
(1)什么是JMM?
Java 内存模型是一种规范,定义了很多东西:
- 所有的变量都存储在主内存(Main Memory)中。
- 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
- 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
- 不同的线程之间无法直接访问对方本地内存中的变量。
JMM:JAVA内存模型,不存在的东西,是一个概念,也是一个约定!
JMM主要是为了规定了线程和内存之间的一些关系,是深入了解Java并发编程的先决条件!
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存;
2、线程加锁前,必须读取主存中的最新值到工作内存中;
3、加锁和解锁是同一把锁;
JMM
中规定内存交互操作有8种:
线程中分为 工作内存、主内存,线程之间要通信必须通过主内存
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中。
- use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令。
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中。
- store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
于是就会产生多线程并发时的线程安全问题:原子性、有序性和可见性
(2)线程安全:原子性、有序性和见性
原子性: 提供互斥访问,同一时刻只能有一个线程对数据进行操作;例如:atomicXXX类,synchronized关键字的应用。
有序性: 一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序;例如,happens-before原则。
可见性: 一个线程对主内存的修改可以及时地被其他线程看到;例如:synchronized,volatile。
CAS
- 使用atomic原子类解决原子类性问题:谈起原子性肯定离不开众所周知的Atomic包,JDK里面提供了很多atomic类,
AtomicInteger
,AtomicLong
,AtomicBoolean
等等。
class AtomicIntegerExample {
private static final Logger log = LoggerFactory.getLogger(AtomicIntegerExample.class);
// 请求总数
public static int requestTotal = 500;
// 并发执行的线程数
public static int threadTotal = 20;
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();//获取线程池
final Semaphore semaphore = new Semaphore(threadTotal);//定义信号量
final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
for (int i = 0; i < requestTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count.get());
}
private static void add() {
count.incrementAndGet();
}
}
AtomicInteger
中的incrementAndGet
方法就是乐观锁的一个实现,CAS:比较当前|工作内存|中的值 和 |主内存|中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。
关键方法:incrementAndGet
()
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//获取当前对象内存的值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
首先通过调用getIntVolatile()方法,使用对象的引用与值的偏移量得到当前值,然后调用compareAndSwapInt检测如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作。
compareAndSwapInt()希望达到的目标是对于var1对象,如果当前的值var2和底层的值var5相等,那么把它更新成后面的值(var5+var4).
原子类可以解决原子性问题,当还是会ABA问题。可以用版本号来解决;
(2)对Volatile 的理解
Volatile 是 Java 虚拟机提供 轻量级的同步机制
-
volatile可以保证可见性;
一个线程对主内存的修改可以及时的被其他线程看到;
-
不能保证原子性
- 互斥访问,同一时刻只能有一个线程对数据进行操作,如atomic类、synchronize关键字的应用
- 原子性的底层核心思想是CAS,但是CAS中存在ABA问题(可用版本号来解决->乐观锁)
-
避免指令重排。由于内存屏障,可以保证避免指令重排的现象产生
- 一个线程观察其他线程中的指令顺序,由于指令重排,改观察结果一般杂乱无章
- volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。
可以使用原子类atomic
这些类的底层都直接和操作系统挂钩!是在内存中修改值。
原子性的底层核心思想是CAS,但是CAS中存在ABA问题(可用版本号来解决->乐观锁)