一、JMM模型与volatile详解
二、synchronized原理详解
三、AQS框架详解——AbstractQueuedSynchronizer
四、ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue和DelayQueue学习总结
五、CountDownLatch、CyclicBarrier和Semaphore的比较
java中interruptor理解与使用
Java线程的6种状态及切换
MESI协议:保证可见性,无法保证原子性
java并发编程脑图
JMM是java内存模型的简称,是一个抽象的概念,它是围绕原子性、有序性、可见性展开的。
(1)主内存是共享内存区域,所有变量都存在主内存中,所有线程都可以访问;
(2)各个线程的工作线程相互独立和隔离;
(3)线程操作变量必须先从主内存中读取变量信息,然后在工作内存中进行修改,最后再写会主内存中,修改才会生效。
从硬件层面描述一下多线程执行过程:
cpu中一个核执行线程A,发现需要用到数据a,就会首先在寄存器中查找,发现没有,就会接着去L1中寻找,接着找L2,L3,如果还没有就到主内存中加载数据,计算完成后,再写回主内存中。
(1)lock
(2)unlock
(3)read
(4)load
(5)use
(6)assign
(7)store
(8)write
一个操作过程是不可中断,一旦开始操作,中间不能被其他线程干扰。
比如java对于基本数据类型的操作是原子性的,但是对于long/double等数据类型的操作是非原子性的。
volatile无法保证原子性
package com.并发专题.writereadlock;
import java.util.concurrent.CountDownLatch;
/**
* @author
* @date 2020/9/4
*/
public class VolatileNotAtomic {
private volatile static int count = 0;
private static Object object =new Object();
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 10; j++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++;
}
}
});
thread.start();
}
//这里主线先睡眠2s,等待其他线程完成操作;此处不睡眠的话,结果不正确
Thread.sleep(2000);
System.out.println(count);
}
}
synchronized和lock可以实现原子性操作。
synchronized和lock通过加锁,使得同一时间只有一个线程访问共享变量,实现原子性和可见性。
可见性是线程A修改了某个共享变量c,线程B能够马上感知到。
package com.volatile关键字可见性;
/**
* @author
* @date 2020/9/2
*/
public class TestVolatileVisible {
private static boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("线程A开始工作");
while(!initFlag){
}
System.out.println("线程A====感觉到flag发生了变化。。。。。");
},"threadA").start();
Thread.sleep(5000);
new Thread(()->{
refreshFlag();
},"threadB").start();
}
private static void refreshFlag() {
initFlag = true;
}
}
volatile、synchronized和lock可以实现可见性。
volatile字节码层面通过ACC_*实现可见性;
使用javap -v TestVolatileVisible.class查看字节码文件。
flags: ACC_STATIC
硬件层面通过:LOCK指令实现;
早期LOCK指令直接锁住总线,
后来优化为MESI缓存一致性协议实现
CAS的形式修改中内存中的值。
了解有序性之前,我们需要首先知道java中会存在指令重排现象,因为cpu和IO之间的速率差异,再汇编层面甚至更底层会在不影响代码执行结果准确性的前提下,对我们的代码重新排序执行。由于某种原因(我觉得是硬件重排序做的不够完美)导致再多线程下,这种从排序会带来执行结果的不确定性,所以我们在多线程场景下编写代码时,需要自己控制代码的有序性,禁止底层的指令重排。
java内存模型也规定了一些先天的代码先后顺序,我们称为“happens-before”原则。但是该原则之外的指令,cpu是可以进行重新排序的。
(1)保证可见性
(2)禁止指令重排
1、volatile:java代码
2、ACC_VOLATILE:字节码
3、JVM内存屏障 :规范
4、hospot实现;C++代码
JVM层面是通过内存屏障实现的。
CPU层面通过LOCK指令实现,Lock指令早起是通过对总线加锁实现一致性,后来通过MESI协议实现同步。
如果在代码中间插入一个内存屏障,那么不管什么指令都不能和这个屏障进行排序,可以实现禁止内存屏障前后指令排序的功能;
内存屏障还能强制刷出各种CPU的缓存数据,让其他线程中的共享变量失效。
private static DCL instance;
虽然有双重锁保证了单例模式,但是这种写法在多线程场景下依然有问题,原因就是:
instance=new DCL();
这句java语法在汇编层面是三条语句:
1、memory= allocate()//申请内存
2、instance(memory)//初始化对象
3、instance=memory //指针指向
很不幸,在happens-before原则下,指令2和3可以重排;多线程场景下会导致其他线程获取到没有完全实例话的对象,引起问题。
这时候我们就可以将代码修改为;
private volatile static DCL instance;
通过volatile关键字来禁止指令2和指令3的重排。