文章目录
一、JMM讲解
1、什么是JMM?
1.1、JMM的简介:
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
1.2、JMM关于同步规定:
a、线程解锁前,必须把共享变量的值刷新回主内存。
b、线程加锁前,必须读取主内存的最新值到自己的工作内存。
c、加锁解锁是同一把锁。
1.3、底层原理:
JVM运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行;首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝。
2、JMM的底层访问过程图:
3、JMM的三大特点:
a、保证可见性。
b、保证原子性。
c、保证有序性。
二、volatile 讲解
1、什么是volatile?
volatile 是 Java 虚拟机提供的轻量级的同步机制,常被称为"轻量级的synchronized"。
2、volatile 的特点
a、保证可见性。
b、保证有序性(禁止指令重排)。
c、不保证原子性。
2.1、代码案例演示:
2.1.1 volatile可见性代码演示:
import java.util.concurrent.TimeUnit;
class MyData{
boolean flag = false; // 没有加volatile ,就没有可见性
//volatile boolean flag = false; // 加了volatile ,就有可见性
public void modifyTrue(){
flag = true;
}
}
/**
* 验证volatile的可见性
*/
public class VolatileDemoTest {
public static void main(String[] args) {
MyData myData = new MyData();
System.out.println(Thread.currentThread().getName() + "\t 开始");
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 线程进入");
//暂停一会儿线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.modifyTrue();
System.out.println(Thread.currentThread().getName() + "\t 线程修改结束, number = "+myData.flag);
}, "AAA").start();
// 第2个线程 main线程
while (myData.flag == false){
// main线程一致循环等待,直到flag不为false
}
System.out.println(Thread.currentThread().getName() + "\t 主线程结束number = "+myData.flag);
}
}
a、当代码中 boolean flag = false; 前面没有加 volatile关键字的时候,程序没有退出;说明线程AAA将flag修改成了true,然后没有通知到主线程的flag,导致程序一直不退出。
b、当代码中 volatile boolean flag = false; 代码前面加了关键字volatile,当线程AAA将flag修改成了true时,将修改的结果通知主线程,主线程收到消息后,就退出循环,程序结束退出。
2.1.2、volatile不保证原子性代码演示:
class MyData{
volatile int number = 0;
// 此时number 前面加了volatile 关键字修饰,volatile不保证原子性
public void addPlus(){
number ++;
}
}
/**
* 验证volatile不保证原子性
* 1.原子性指的是什么意思:
* 不可分割, 完整性, 也即某个线程正在做某个具体业务时,需要整体完整性
* 要么同时成功,要么同时失败
*
* 4.如何解决
* 4.1 加上synchronized,但是synchronized太重,杀鸡不用牛刀
* 4.2 使用AtomicInteger(推荐)
*/
public class VolatileNoAtomic {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.addPlus();
}
}, String.valueOf(i)).start();
}
//需要等待上面20个线程计算完成后,再用main取得最终结果
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "最终 volatile number:" +myData.number);
}
}
a、运行结果:如果volatile保证原子性,那么运行结果应该是20*1000 等于20000 才是正确的。
b、那么如何保证原子性呢?
第一:可以使用加上synchronized关键字,但是不推荐,因为处理一件小的事情,用加上synchronized关键字重量级的锁,有点得不偿失。
第二:推荐使用AtomicInteger,AtomicInteger底层是通过CAS和Unsafe类来保证原子性的,没有实际加锁,而是使用了自旋锁的思想。
AtomicInteger 保证原子性代码演示:
import java.util.concurrent.atomic.AtomicInteger;
class MyData{
volatile int number = 0;
// 此时number 前面加了volatile 关键字修饰,volatile不保证原子性
public void addPlus(){
number ++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
/**
* 验证volatile不保证原子性
* 1.原子性指的是什么意思:
* 不可分割, 完整性, 也即某个线程正在做某个具体业务时,需要整体完整性
* 要么同时成功,要么同时失败
*
* 2.如何解决
* 2.1 加上synchronized,但是synchronized太重,杀鸡不用牛刀
* 2.2 使用AtomicInteger(推荐)
*/
public class VolatileNoAtomic {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.addPlus();
myData.addMyAtomic();
}
}, String.valueOf(i)).start();
}
//需要等待上面20个线程计算完成后,再用main取得最终结果
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "最终 volatile number:" +myData.number);
System.out.println(Thread.currentThread().getName() + "最终 AtomicInteger number:" +myData.atomicInteger);
}
}
运行结果:使用volatie 修饰的不保证原子性,结果小于20000;使用AtomicInteger修饰的保证原子性,结果等于20000.
2.1.3、volatile 有序性(禁止指令重排)演示和总结:
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 构造方法");
}
/**
* 双重检测机制
*
* @return
*/
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
总结:
a、private static volatile SingletonDemo instance = null; instance 前面加了volatile可以禁止指令重排,比如代码中 instance = new SingletonDemo();这行代码,执行的顺序应该是:
1.为instance 分配内存空间;
2.初始化instance ;
3.将instance 指向分配的内存地址。
如果程序发生指令重排,顺序可能是1->3->2; 这样当对象被调用的时候,虽然对象不为空,但是没有初始化。
b、因此volatile可以禁止指令重排,保证程序在多线程环境下也可以安全运行。
c、详解:
DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
instance=new SingletonDem(); 可以分为以下步骤(伪代码)
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.初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题.
3、volatile 的示例
1、面试官:项目中有用到valotile吗?
答:一般用于 单例模式DCL代码 (即双重校验锁机制),如上面 2.1.3 的禁止指令重排的代码。
4、volatile关键字和synchronized 关键字比较
a、volatile关键字的性能要优于synchronized关键字,因为volatile关键字是线程同步的轻量级实现,而synchronized关键字是重量级的。
b、volatile关键字只能作用于变量;而synchronized关键字可以修饰方法和代码块。
c、多线程访问volatile关键字不会发生阻塞,而访问synchronized关键字可能会发生阻塞。
d、volatile关键字保证了数据的可见性,而不能保证数据的原子性;而synchronized关键字两者都可以。
e、volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字主要用于解决多线程之间访问资源的同步性。