第一、java内存模型
共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,
每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
JMM关于同步的规定:
1 线程解锁前,必须把共享变量的值刷新回主内存
2线程加锁前,必须读取主内存的最新值到自己的工作内存
3加锁解锁是同一把锁
由于JVM运行程序的实体就是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方为称为栈空间),工作内存是每个线程的私有数据区域,
而Java内存模型总规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,
首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再讲变量写回主内存,不能直接操作主内存中的变量,
各个线程中的工作内存存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,这个就是可见性
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。
当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,
可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。
第二、Vilatile的特性
2.1.vilatile定义
volatile是java虚拟机提供的轻量级的同步机制
2.2.可见性
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后写回到主内存中的。这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,
另外一个线程BBB有对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说是并不可见的,这种工作内存与主内存同步延迟线程就造成了可见性问题,
代码案例:
class MyData{
volatile int number = 1;
public void updateNumber(){
this.number = 60;
}
}
/**
* 验证volatitle的可见性
*
* 1.1假如int number =0;number 变量之前根本没有添加volatile关键字修饰
1.2添加volatile关键修饰
*/
public class VolatileDemo {
public static void main(String[] args) throws Exception{
MyData myData = new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" common in");
//暂停一会儿线程
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.updateNumber();
System.out.println(Thread.currentThread().getName()+" update number "+myData.number);
},"AA").start();
while (myData.number==1){
//等于0一直等待
}
System.out.println("main number over "+myData.number);
}
}
2.3.不保证原子性
原子性:不可分割,完整性,也即某个线程正在做某个业务时,中间不可以被加载或者被分割。需要整体完整要么同时成功,要么同时失败,
也就是最终一致性
验证可见性和不保证原子性
不保证原子性的解决方法:
1.加sync
2.直接使用JUC下AtomicInteger
class MyData{
volatile int number = 0;
public void addNumber(){
this.number = 60;
}
public void addAtomicity(){
this.number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
/**
* 1验证volatile的可见性
* 1.1假如int number=0;number变量之前根本没有添加volatile关键字修饰,没有可见性
* 1.2添加了volatile,可以解决可见性问题
* 2验证volatitle不保证原子性
* 原子性:不可分割,完整性,也即某个线程正在做某个业务时,中间不可以被加载或者被分割。
* 需要整体完整要么同时成功,要么同时失败
*
*/
public class VolatileDemo01 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
myData.addAtomicity();
myData.addMyAtomic();
}
}
}).start();
}
//需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果看是多少?
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"最终结果值:"+myData.number);
System.out.println(Thread.currentThread().getName()+"最终结果值:"+myData.atomicInteger);
}
//volatile可见保证可见性,及时通知其他线程,主物理内存的值以及被修改了
public static void seeVolatile(){
MyData myData = new MyData();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("修改之前的值:"+myData.number);
//等待三秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用改变的方法
myData.addNumber();
System.out.println("修改之后的值:"+myData.number);
}
}).start();
while(myData.number==0){
//一直循环等待
}
System.out.println(Thread.currentThread().getName()+":"+myData.number);
}
}
2.4.禁止指令重排
volatile 禁止指令重排
JMM 有序性
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令重排,一般分为以下3种
源代码→编译器优化的重排→内存系统的重排→最终执行的指令
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保住一致性是无法确定的,结果无法预测
禁止指令重排总结
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一下概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序,
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和折腾Memory Barrier指令重排序,
也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
JMM线程安全得到保证
工作内存和主内存同步延时现象导致的可见性问题,可以使用synchronize或者volatile关键字进行解决,他们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化
那些地方用过volatile
1.单例模式DCL代码 2单例模式volatile分析
public class VolatileDemo02 {
private static volatile VolatileDemo02 instance = null;
private VolatileDemo02(){
System.out.println(Thread.currentThread().getName()+"构造函数");
}
//DCL(Double Check Lock双端检测机制)
public static VolatileDemo02 getInstance(){
if (instance == null) {
synchronized (VolatileDemo02.class){
if (instance == null) {
instance = new VolatileDemo02();
}
}
}
return instance;
}
public static void main(String[] args) {
// System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
// System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
// System.out.println(VolatileDemo02.getInstance()==VolatileDemo02.getInstance());
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
VolatileDemo02.getInstance();
}
}).start();
}
}
}
DCL(Double Check Lock双端检测机制)
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初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但不会关系多线程间的语义一致性。
所以当一条数据线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
2.5Volatile与Synchronized区别
(1)从而我们可以看出volatile虽然具有可见性但是并不能保证原子性。
(2)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。
但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。
synchronized太重了