- JUC(java.util.concurrent)
- 进程和线程
- 进程:后台运行的程序(我们打开的一个软件,就是进程)
- 线程:轻量级的进程,并且一个进程包含多个线程(同在一个软件内,同时运行窗口,就是线程)
- 并发和并行
- 并发:同时访问某个东西,就是并发
- 并行:一起做某些事情,就是并行
- 进程和线程
- JUC下的三个包
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
- java.util.concurrent
谈谈对Volatile的理解
Volatile在日常的单线程环境是用不到的
Volatile是java虚拟机提供的轻量级的同步机制(主要有三大特性)
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM是什么
JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值,到自己的工作内存
- 加锁和解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写会主内存
,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程:
数据传输速率:硬盘 < 内存 < < cache < CPU
上面提到了两个概念:主内存 和 工作内存
-
主内存:就是计算机的内存,也就是经常提到的8G内存,16G内存
-
工作内存:但我们实例化 new student,那么 age = 25 也是存储在主内存中
- 当同时有三个线程同时访问 student中的age变量时,那么每个线程都会拷贝一份,到各自的工作内存,从而实现了变量的拷贝
即:JMM内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。
缓存一致性
为什么这里主线程中某个值被更改后,其它线程能马上知晓呢?其实这里是用到了总线嗅探技术
在说嗅探技术之前,首先谈谈缓存一致性的问题,就是当多个处理器运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存数据不一。
为了解决缓存一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,这类协议主要有MSI、MESI等等。
MESI
当CPU写数据时,如果发现操作的变量是共享变量,即在其它CPU中也存在该变量的副本,会发出信号通知其它CPU将该内存变量的缓存行设置为无效,因此当其它CPU读取这个变量的时,发现自己缓存该变量的缓存行是无效的,那么它就会从内存中重新读取。
总线嗅探
那么是如何发现数据是否失效呢?
这里是用到了总线嗅探技术,就是每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从内存中把数据读取到处理器缓存中。
总线风暴
总线嗅探技术有哪些缺点?
由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS循环,无效的交互会导致总线带宽达到峰值。因此不要大量使用volatile关键字,至于什么时候使用volatile、什么时候用锁以及Syschonized都是需要根据实际场景的。
JMM的特性
JMM的三大特性,volatile只保证了两个,即可见性和有序性,不满足原子性
- 可见性
- 原子性
- 有序性
可见性代码验证
/**
* @author HX
* @title: VolatileDemo
* @projectName spring_cloud1x
* @date 2022/5/7 13:58
* 证明Volatile的可见性
*/
public class VolatileDemo {
public static volatile Integer num=0;
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 进来了");
//睡3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改num值
num=60;
System.out.println(Thread.currentThread().getName()+"\t 修改num值:"+num);
},"AAAA").start();
//若不加Volatile while将会死循环,修改后num值不会及时通知
while (num==0){
//如果num值等于0,一直等待
}
System.out.println(Thread.currentThread().getName()+"\t 结束 num值:"+num);
}
}
不保证原子性验证:
package com.xiaoxx.rabbitmq.test.juc.manager;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author HX
* @title: VolatileDemo
* @projectName spring_cloud1x
* @date 2022/5/7 13:58
* 证明Volatile的可见性和不保证原子性
* <p>
* 原子性:不可分割完整性,即某个线程在做具体业务时,不能被加塞或分割。需要整体完整,要么同时成功,要么同时失败
*/
public class VolatileDemo {
public static volatile Integer num = 0;
private static String a = "锁";
public static AtomicInteger num1 = new AtomicInteger(0);
public static void main(String[] args) {
//证明Volatile不保证原子性
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
num++;
num1.incrementAndGet();
// synchronized (a) {
// num++;
// }
}
}, "线程" + i).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("最后结果: \n不保证原子性结果" + num + "\n保证原子性结果" + num1.get());
}
//证明Volatile的可见性
private static void seeOkByVolatile() {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 进来了");
//睡3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改num值
num = 60;
System.out.println(Thread.currentThread().getName() + "\t 修改num值:" + num);
}, "AAAA").start();
//若不加Volatile while将会死循环,修改后num值不会及时通知
while (num == 0) {
//如果num值等于0,一直等待
}
System.out.println(Thread.currentThread().getName() + "\t 结束 num值:" + num);
}
}
Volatile的应用
单例模式DCL代码
首先回顾一下,单线程下的单例模式代码
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
public SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
instance = new SingletonDemo();
return instance;
}
public static void main(String[] args) {
// System.out.println(getInstance()==getInstance());
// System.out.println(getInstance()==getInstance());
// System.out.println(getInstance()==getInstance());
for (int i = 0; i < 100; i++) {
new Thread(() -> {
getInstance();
}, "我是线程" + i).start();
}
}
}
DCL(Double Check Lock 双端检锁机制)代码:
package com.xiaoxx.rabbitmq.test.juc.manager;
/**
* @author HX
* @title: SingletonDemo
* @projectName spring_cloud1x
* @date 2022/5/7 16:15
* 单例模式
*/
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
public SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
//DCL(Double check lock双端解锁机制)
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
// System.out.println(getInstance()==getInstance());
// System.out.println(getInstance()==getInstance());
// System.out.println(getInstance()==getInstance());
for (int i = 0; i < 100; i++) {
new Thread(() -> {
getInstance();
}, "我是线程" + i).start();
}
}
}
从输出结果来看,确实能够保证单例模式的正确性,但是上面的方法还是存在问题的
DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:
- memory = allocate(); // 1、分配对象内存空间
- instance(memory); // 2、初始化对象
- instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
- memory = allocate(); // 1、分配对象内存空间
- instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
- instance(memory); // 2、初始化对象
这样就会造成什么问题呢?
也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例
指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题
所以需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性