网林时代网站建设,acaa平面设计师证书报名费,大型网站制作丹阳网站建设,购物网站asp源码文章目录1.保证内存可见性2.可见性验证3.原子性验证4.原子性问题解决5.禁止指令重排序6.JMM谈谈你的理解6.1.基本概念6.2.JMM同步规定6.2.1.可见性6.2.2.原子性6.2.3.有序性6.3.Volatile针对指令重排做了啥7.你在哪些地方用过Volatile#xff1f;volatile是Java提供的轻量级的…
文章目录1.保证内存可见性2.可见性验证3.原子性验证4.原子性问题解决5.禁止指令重排序6.JMM谈谈你的理解6.1.基本概念6.2.JMM同步规定6.2.1.可见性6.2.2.原子性6.2.3.有序性6.3.Volatile针对指令重排做了啥7.你在哪些地方用过Volatilevolatile是Java提供的轻量级的同步机制主要有三个特性1.保证内存可见性 2.不保证原子性 3.禁止指令重排序 1.保证内存可见性
volatile是Java提供的轻量级的同步机制保证了可见性不保证原子性。了解volatile工作机制首先要对Java内存模型JMM有初步的认识
每个线程创建时JVM会为其创建一份私有的工作内存栈空间不同线程的工作内存之间不能直接互相访问。JMM规定所有的变量都存在主内存主内存是共享内存区域所有线程都可以访问线程对变量进行读写会从主内存拷贝一份副本到自己的工作内存操作完毕后刷新到主内存。所以线程间的通信要通过主内存来实现。
volatile的作用是线程对副本变量进行修改后其他线程能够立刻同步刷新最新的数值。这个就是可见性。 2.可见性验证
我们来看一个例子
package com.bruceliu.demo15;import java.util.concurrent.TimeUnit;/*** BelongsProject: Thread0509* BelongsPackage: com.bruceliu.demo15* Author: bruceliu* QQ:1241488705* CreateTime: 2020-05-13 23:16* Description: TODO*/
public class VolatileDemo {int x 0;//注意这里的b没有被volatile修饰boolean b false;/*** 写操作*/private void write() {x 5;b true;System.out.println(x x);System.out.println(b b);}/*** 读操作*/private void read() {//如果bfalse的话就会无限循环直到btrue才会执行结束会打印出x的值while (!b) {}System.out.println(x x);}public static void main(String[] args) throws Exception {final VolatileDemo volatileDemo new VolatileDemo();//线程1执行写操作Thread thread1 new Thread(new Runnable() {public void run() {volatileDemo.write();}});//线程2执行读操作Thread thread2 new Thread(new Runnable() {public void run() {volatileDemo.read();}});//我们让线程2的读操作先执行thread2.start();//睡1毫秒为了保证线程2比线程1先执行TimeUnit.MILLISECONDS.sleep(1);//再让线程1的写操作执行thread1.start();thread1.join();thread2.join();//等待线程1和线程2全部结束后打印执行结束System.out.println(执行结束);}
}注意我们的b没有用volatile修饰我们先启动了线程2的读操作后启动了线程1的写操作由于线程1和线程2会保存x和b的副本到自己的工作内存中线程2执行后由于他副本bfalse所以会进入到无限循环中线程1执行后修改的也是自己副本中的btrue然而线程2无法立即察觉到所以执行上面代码后不会打印“执行结束”因为线程2一直在执行
运行之后会一直出于运行状态并且没有打印“执行结束” 此时的流程会是这样子 给b加了volatile关键字修饰后线程1对b做了修改然后会立即更新内存中的值线程2通过嗅探发现自己的副本已经过期了然后重新从内存中拿到btrue的值然后跳出while循环执行结束 我们知道volatile关键字的作用是保证变量在多线程之间的可见性它是java.util.concurrent包的核心没有volatile就没有这么多的并发类给我们使用. 3.原子性验证
看下面一段代码number变量加了volatile修饰。创建了10个子线程每个线程循环1000次执行number。
package com.bruce.demo8;import java.util.concurrent.TimeUnit;/*** BelongsProject: SingleTon-2020* BelongsPackage: com.bruce.demo8* CreateTime: 2020-12-10 23:08* Description: TODO*/
public class Demo8 {static class MyTest {public volatile int number 0;public void incr(){number;}}public static void main(String[] args) {MyTest myTest new MyTest();for (int i 1; i 10; i){new Thread(() - {for (int j 1; j 1000; j){myTest.incr();}}, ThreadString.valueOf(i)).start();}try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}//等线程执行结束了输出number值System.out.println(当前number myTest.number);}
}
按理说number最终应该是10000但是这边执行后结果如下
4.原子性问题解决
方法一使用 synchronized 关键字
//给函数增加synchronized修饰相当于加锁了public synchronized void incr(){number;}结果如下 方法二使用AtomicInteger
public class Demo8 {static class MyTest {public volatile AtomicInteger number new AtomicInteger();public void incr(){number.getAndIncrement();}}public static void main(String[] args) {MyTest myTest new MyTest();for (int i 1; i 10; i){new Thread(() - {for (int j 1; j 1000; j){myTest.incr();}}, ThreadString.valueOf(i)).start();}try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}//等线程执行结束了输出number值System.out.println(当前number myTest.number);}
}5.禁止指令重排序
体现了JMM的有序性
6.JMM谈谈你的理解
6.1.基本概念
JMM 本身是一种抽象的概念并不是真实存在它描述的是一组规定或则规范通过这组规范定义了程序中的访问方式。
6.2.JMM同步规定
6.2.1.可见性
可见性一个线程对某一共享变量修改之后另一个线程要立即获取到修改后的结果。
线程解锁前必须把共享变量的值刷新回主内存线程加锁前必须读取主内存的最新值到自己的工作内存加锁解锁是同一把锁
由于 JVM 运行程序的实体是线程而每个线程创建时 JVM 都会为其创建一个工作内存工作内存是每个线程的私有数据区域而 Java 内存模型中规定所有变量的储存在主内存主内存是共享内存区域所有的线程都可以访问但线程对变量的操作读取赋值等必须都工作内存进行看。
首先要将变量从主内存拷贝的自己的工作内存空间然后对变量进行操作操作完成后再将变量写回主内存不能直接操作主内存中的变量工作内存中存储着主内存中的变量副本拷贝前面说过工作内存是每个线程的私有数据区域因此不同的线程间无法访问对方的工作内存线程间的通信(传值)必须通过主内存来完成。
内存模型图
6.2.2.原子性
原子性 不可分割完整性也就是说某个线程正在做某个具体业务时中间不可以被加塞或者被分割需要具体完成要么同时成功要么同时失败。
数据库也经常提到事务具备原子性! 在putfield时其他线程挂起没有及时得到主内存的值改变消息 n在多线程下是非线程安全的如何不加synchronized解决
使用原子类(java.util.concurrent.atomic)为什么使用了原子类可以解决原理是什么CAS
6.2.3.有序性
计算机在执行程序时为了提高性能编译器和处理器常常会对指令做重排一般分一下3种 源代码-编译器优化的重排-指令并行的重排-内存系统的重排-最终执行的指令 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须考虑指令之间的数据依赖性。
多线程环境中线程交替执行由于编译器优化重排的存在两个线程中使用的变量能否保证一致性是无法确定的结果无法预测。
指令重排 - example 1
public void mySort() {int x 11; // 1int y 12; // 2x x 5; // 3y x * x; // 4}按照正常单线程环境执行顺序是 1 2 3 4 但是在多线程环境下可能出现以下的顺序
2 1 3 41 3 2 4上述的过程就可以当做是指令的重排即内部执行顺序和我们的代码顺序不一样。但是指令重排也是有限制的即不会出现下面的顺序
4 3 2 1因为处理器在进行重排时候必须考虑到指令之间的数据依赖性
因为步骤 4需要依赖于 y的申明以及x的申明故因为存在数据依赖无法首先执行
例子 int a,b,x,y 0
线程1线程2x a;y b;b 1;a 2;x 0; y 0
因为上面的代码不存在数据的依赖性因此编译器可能对数据进行重排
线程1线程2b 1;a 2;x a;y b;x 2; y 1
这样造成的结果和最开始的就不一致了这就是导致重排后结果和最开始的不一样因此为了防止这种结果出现volatile就规定禁止指令重排为了保证数据的一致性
指令重排 - example 2 比如下面这段代码
public class ResortSeqDemo {int a 0;boolean flag false;public void method01() {a 1;flag true;}public void method02() {if(flag) {a a 5;System.out.println(reValue: a);}}
}我们按照正常的顺序分别调用method01() 和 method02() 那么最终输出就是 a 6
但是如果在多线程环境下因为方法1 和 方法2他们之间不能存在数据依赖的问题因此原先的顺序可能是
a 1;
flag true;a a 5;
System.out.println(reValue: a);但是在经过编译器指令或者内存的重排后可能会出现这样的情况
flag true;a a 5;
System.out.println(reValue: a);a 1;也就是先执行 flag true后另外一个线程马上调用方法2满足 flag的判断最终让a 5结果为5这样同样出现了数据不一致的问题
为什么会出现这个结果多线程环境中线程交替执行由于编译器优化重排的存在两个线程中使用的变量能否保证一致性是无法确定的结果无法预测。
这样就需要通过volatile来修饰来禁止指令重排序保证线程安全性
6.3.Volatile针对指令重排做了啥
Volatile实现禁止指令重排优化从而避免了多线程环境下程序出现乱序执行的现象 首先了解一个概念内存屏障Memory Barrier又称内存栅栏是一个CPU指令它的作用有两个
保证特定操作的顺序保证某些变量的内存可见性利用该特性实现volatile的内存可见性
由于编译器和处理器都能执行指令重排的优化如果在指令间插入一条Memory Barrier则会告诉编译器和CPU不管什么指令都不能和这条Memory Barrier指令重排序也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是刷新出各种CPU的缓存数因此任何CPU上的线程都能读取到这些数据的最新版本。 也就是过在Volatile的写 和 读的时候加入屏障防止出现指令重排的
7.你在哪些地方用过Volatile
工作内存与主内存同步延迟现象导致的可见性问题
可通过synchronized或volatile关键字解决他们都可以使一个线程修改后的变量立即对其它线程可见对于指令重排导致的可见性问题和有序性问题可以使用volatile关键字解决因为volatile关键字的另一个作用就是禁止重排序优化
举例
public class LazySafe {private LazySafe(){}//对象加上了volatile关键字是为了保证变量的可见性防止指令重排序//第二个线程拿到的可能是半实列化的对象所以要加volatile防止指令重排序private volatile static LazySafe lazySafe;public static LazySafe getInstance(){if(lazySafenull){//双重判定synchronized (LazySafe.class){if(lazySafenull){lazySafenew LazySafe(); //不是原子性的}}}return lazySafe;}
}DCL双端检锁机制不一定是线程安全的原因是有指令重排的存在加入volatile可以禁止指令重排
原因是在某一个线程执行到第一次检测的时候读取到 instance 不为nullinstance的引用对象可能没有完成实例化。因为 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来保证出现指令重排的问题从而保证单例模式的线程安全性