了解 JMM(Java内存模型),链接:JMM(Java内存模型)浅记
了解volatile和synchronized关键字
volatile关键字:
synchronized关键字:
注意:
虽然 volatile 和 synchronized 都实现了 JMM 规范的有序性,但是两者实现的方式有所不同,volatile通过禁止指令重排来保证有序性,synchronized 通过限制每次只能一个线程执行加锁的代码的操作来保证有序性。
翻译过来就是,比较并交换。
介绍:
JMM规范中有主内存和工作内存(即线程的独占内存)之分。
工作内存在往主内存写回数据之前,根据期望值(之前获得主内存的数据)和现在主内存的数值进行比较,如果两者的值相同,则说明没有其他线程修改主内存中值,此时当前线程可以将自己的工作内存数据写回主内存。反之,期望值和现在主内存的值不相等,说明主内存数据被其他线程更改过了,所以更新期望值为主内存中的数值,在下一次写回主内存中,使用新的期望值和现在主内存中数据比较。往复循环,直至工作内存中的数据写回主内存中。
简单来说:
比较并交换,即是通过期望值和当前值进行比较,如果比较两者相同,则修改主内存中的值,否则不修改,直至修改成功。
案例:
当我们多线程进行 自增操作时,会发生数据覆盖,导致得到的结果不正确,可以采用如下两种方式来解决:
原因:
synchronized 是一把重型锁,影响性能,不适用这种场景。
补充:
volatile虽然是轻量级的同步机制,但是其不保证原子性,所以无法解决多线程自增,数据发生覆盖的问题。
代码演示如下:
多线程执行自增:
@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomicIntegerTest {
private Logger logger = LoggerFactory.getLogger(AtomicIntegerTest.class);
//正常的定义变量
private int[] a = {0};
private AtomicInteger atomic = new AtomicInteger();
@Test
public void test1() throws InterruptedException {
for (int i = 0; i < 18; i++){
new Thread(()->{
//logger.info("当前线程的名称:{},开始",Thread.currentThread().getName());
for (int j = 0; j < 1000; j++){
a[0]++;
}
logger.info("当前线程的名称:{},结束",Thread.currentThread().getName());
}, String.valueOf(i)).start();
}
//休眠5秒,保证上面各线程执行完成
TimeUnit.SECONDS.sleep(5);
logger.info("得到的最终结果:{}", a[0]);
}
}
得到的结果:
由于 i++ 不是原子性操作,会存在覆盖的情况,导致结果小于 18000 。
使用volatile
@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomicIntegerTest {
private Logger logger = LoggerFactory.getLogger(AtomicIntegerTest.class);
//使用volatile
volatile private int[] a = {0};
private AtomicInteger atomic = new AtomicInteger();
@Test
public void test1() throws InterruptedException {
for (int i = 0; i < 18; i++){
new Thread(()->{
//logger.info("当前线程的名称:{},开始",Thread.currentThread().getName());
for (int j = 0; j < 1000; j++){
a[0]++;
}
logger.info("当前线程的名称:{},结束",Thread.currentThread().getName());
}, String.valueOf(i)).start();
}
//休眠5秒,保证上面各线程执行完成
TimeUnit.SECONDS.sleep(5);
logger.info("得到的最终结果:{}", a[0]);
}
}
可见 volatile 关键字不能保证原子性。
使用synchronized
@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomicIntegerTest {
private Logger logger = LoggerFactory.getLogger(AtomicIntegerTest.class);
private int[] a = {0};
//原子操作类,不传入参数,默认是0
private AtomicInteger atomic = new AtomicInteger();
@Test
public void test2() throws InterruptedException {
for (int i = 0; i < 18; i++){
new Thread(()->{
//logger.info("当前线程的名称:{},开始",Thread.currentThread().getName());
for (int j = 0; j < 1000; j++){
//使用synchronized
synchronized (this){
a[0]++;
}
}
logger.info("当前线程的名称:{},结束",Thread.currentThread().getName());
}, String.valueOf(i)).start();
}
//休眠5秒,保证上面各线程执行完成
TimeUnit.SECONDS.sleep(5);
logger.info("得到的最终结果:{}", a[0]);
}
}
原子操作类AtomicInteger
@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomicIntegerTest {
private Logger logger = LoggerFactory.getLogger(AtomicIntegerTest.class);
private int[] a = {0};
//原子操作类,不传入参数,默认是0
private AtomicInteger atomic = new AtomicInteger();
@Test
public void test3() throws InterruptedException {
for (int i = 0; i < 20; i++){
new Thread(()->{
//logger.info("当前线程的名称:{},开始",Thread.currentThread().getName());
for (int j = 0; j < 1000; j++){
//使用原子操作类的方法进行自增
atomic.getAndIncrement();
}
logger.info("当前线程的名称:{},结束",Thread.currentThread().getName());
}, String.valueOf(i)).start();
}
//休眠5秒,保证上面各线程执行完成
TimeUnit.SECONDS.sleep(5);
logger.info("得到的最终结果:{}", atomic.get());
}
}
结果正常,和使用 synchronized 效果相同。
AtomicInteger对象调用自增的方法
atomic.getAndIncrement();
AtomicInteger中的源码
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
进入到 rt.jar 下的 Unsafe 类
//var1:传入的对象,var2:偏移地址, var4:1
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//通过传入的对象和偏移地址,获得主内存的当前数据,作为线程的工作内存的预期值
//方法名中有volatile,具有可见性。
var5 = this.getIntVolatile(var1, var2);
/**
* 当工作内存回写到主内存时,通过 var1 和 var2 获取主内存中 当前数据,判断主内存的当前数据
* 和之前线程的工作内存中预期值 var5 是否相同,如果相同,则把预期值 var5 + 1 (var4),
* 返回true,取反后得到FALSE,循环终止。如果通过var1和var2取得的主内存当前值和预期值不同,
* 返回false,取反之后得到true,继续循环,直至线程写回主内存成功。
*/
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//返回 + var4 (1) 的最终结果
return var5;
}
AtomicInteger底层通过不断自旋,保证得到的结果正确。
CAS 自旋锁和 synchronized 锁优缺点:
CAS:
synchronized:
概述:
一个值从A变成B,然后再变成A,此时两个A虽然数值一样,但是从实质来说,两者不是同一个A。在自旋的过程中,会将ABA中的后一个A当做前一个A。这个问题在一些情况是没问题的,在一些情况是会造成问题的。
问题:
解决:
添加版本号,类似于乐观锁的版本号。
谈到 JMM 需要想到 volatile 和 synchronized 关键字,volatile 要想到 单例模式 、 DCL (双重检查锁)和 原子操作类(比如 AtomicInteger),通过原子操作类要想到其底层 使用了 UnSafe 类(在类中大多数据都是用native修饰,使用了C语言来操作底层硬件系统)和 CAS 自旋锁,CAS 底层用到 CAS 原语以此来保证操作的原子性。 通过原子操作类要想到原子引用。通过 CAS 要想到 ABA 问题,通过ABA问题要想到乐观锁。
注意:
即使我们 java 的一个语句对应 一个编译语言,也不能保证该操作就是原子性操作,因为编译语言还要转换为机器语言,要保证是原子操作,必须要对应的是编译语言的原语(原语要保证操作不被中断,直到操作执行完成)。
注意:
DCL的优缺点待补充。