青岛网站建设q.479185700強,旅游网站开发需求,成都房价,网站建设包含专业文章目录一、简介二、并发编程的3个基本概念1.原子性2.可见性3.有序性三、锁的互斥和可见性四、Java的内存模型JMM以及共享变量的可见性五、volatile变量的特性1.保证可见性#xff0c;不保证原子性2.禁止指令重排六、volatile不适用的场景1.volatile不适合复合操作2.解决方法…
文章目录一、简介二、并发编程的3个基本概念1.原子性2.可见性3.有序性三、锁的互斥和可见性四、Java的内存模型JMM以及共享变量的可见性五、volatile变量的特性1.保证可见性不保证原子性2.禁止指令重排六、volatile不适用的场景1.volatile不适合复合操作2.解决方法七、volatile原理八、单例模式的双重锁为什么要加volatile一、简介
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制同步块或方法和 volatile 变量相比于synchronizedsynchronized通常称为重量级锁volatile更轻量级因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差有时它更简单并且开销更低而且其使用也更容易出错。
二、并发编程的3个基本概念
1.原子性
定义 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断要么就都不执行。
原子性是拒绝多线程操作的不论是多核还是单核具有原子性的量同一时刻只能有一个线程来对它进行操作。简而言之在整个操作过程中不会被线程调度器中断的操作都可认为是原子性。例如 a1是原子性操作但是a和a 1就不是原子性操作。Java中的原子性操作包括
1基本类型的读取和赋值操作且赋值必须是值赋给变量变量之间的相互赋值不是原子性操作。
2所有引用reference的赋值操作
3java.concurrent.Atomic.* 包中所有类的一切操作
2.可见性
定义指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值。
在多线程环境下一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性当一个变量被volatile修饰后表示着线程本地内存无效当一个线程修改共享变量后他会立即被更新到主内存中其他线程读取共享变量时会直接从主内存中读取。当然synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3.有序性
**定义**即程序执行的顺序按照代码的先后顺序执行。
Java内存模型中的有序性可以总结为如果在本线程内观察所有操作都是有序的如果在一个线程中观察另一个线程所有操作都是无序的。前半句是指“线程内表现为串行语义”后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。
在Java内存模型中为了效率是允许编译器和处理器对指令进行重排序当然重排序不会影响单线程的运行结果但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL双重检查锁。另外可以通过synchronized和Lock来保证有序性synchronized和Lock保证每个时刻是有一个线程执行同步代码相当于是让线程顺序执行同步代码自然就保证了有序性。
三、锁的互斥和可见性
锁提供了两种主要特性互斥mutual exclusion 和可见性visibility。
1互斥即一次只允许一个线程持有某个特定的锁一次就只有一个线程能够使用该共享数据。
2可见性要更加复杂一些它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值新值对于其他线程来说是可以立即得知的**。**如果没有同步机制提供的这种可见性保证线程看到的共享变 量可能是修改前的值或不一致的值这将引发许多严重问题。要使 volatile 变量提供理想的线程安全必须同时满足下面两个条件
a.对变量的写操作不依赖于当前值。
b.该变量没有包含在具有其他变量的不变式中。
实际上这些条件表明可以被写入 volatile 变量的这些有效值独立于任何程序的状态包括变量的当前状态。事实上就是保证操作是原子性操作才能保证使用volatile关键字的程序在并发时能够正确执行。
四、Java的内存模型JMM以及共享变量的可见性
JMM决定一个线程对共享变量的写入何时对另一个线程可见JMM定义了线程和主内存之间的抽象关系共享变量存储在主内存(Main Memory)中每个线程都有一个私有的本地内存Local Memory本地内存保存了被该线程使用到的主内存的副本拷贝线程对变量的所有操作都必须在工作内存中进行而不能直接读写主内存中的变量。 对于普通的共享变量来讲线程A将其修改为某个值发生在线程A的本地内存中此时还未同步到主内存中去而线程B已经缓存了该变量的旧值所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题较粗暴的方式自然就是加锁但是此处使用synchronized或者Lock这些方式太重量级了比较合理的方式其实就是volatile。
需要注意的是JMM是个抽象的内存模型所以所谓的本地内存主内存都是抽象概念并不一定就真实的对应cpu缓存和物理内存
五、volatile变量的特性
1.保证可见性不保证原子性
1当写一个volatile变量时JMM会把该线程本地内存中的变量强制刷新到主内存中去
2这个写会操作会导致其他线程中的volatile变量缓存无效。
2.禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则
1重排序操作不会对存在数据依赖关系的操作进行重排序。
比如a1;ba; 这个指令序列由于第二个操作依赖于第一个操作所以在编译时和处理器运行时这两个操作不会被重排序。
2重排序是为了优化性能但是不管怎么重排序单线程下程序的执行结果不能被改变
比如a1;b2;cab这三个操作第一步a1)和第二步(b2)由于不存在数据依赖关系 所以可能会发生重排序但是cab这个操作是不会被重排序的因为需要保证最终的结果一定是cab3。
重排序在单线程下一定能保证结果的正确性但是在多线程环境下可能发生重排序影响结果下例中的1和2由于不存在数据依赖关系则有可能会被重排序先执行statustrue再执行a2。而此时线程B会顺利到达4处而线程A中a2这个操作还未被执行所以ba1的结果也有可能依然等于2。
public class TestVolatile{int a 1;boolean status false;//状态切换为truepublic void changeStatus{a 2; //1status true; //2}//若状态为true则为runningpublic void run(){if(status){ //3int b a 1; //4System.out.println(b);}}}使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量在编译时会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则
a.当程序执行到volatile变量的读操作或者写操作时在其前面的操作的更改肯定全部已经进****行且结果已经对后面的操作可见在其后面的操作肯定还没有进行
b.在进行指令优化时不能将对volatile变量访问的语句放在其后面执行也不能把volatile变量后面的语句放到其前面执行。
即执行到volatile变量时其前面的所有语句都执行完后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
六、volatile不适用的场景
1.volatile不适合复合操作
例如inc不是一个原子性操作可以由读取、加、赋值3步组成所以结果并不能达到30000。. 2.解决方法
1采用synchronized 2采用Lock 3采用java并发包中的原子操作类原子操作类是通过CAS循环的方式来保证其原子性的 七、volatile原理
volatile可以保证线程可见性且提供了一定的有序性但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现加入volatile关键字时会多出一个lock前缀指令lock前缀指令实际上相当于一个内存屏障也成内存栅栏内存屏障会提供3个功能
1它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置也不会把前面的指令排到内存屏障的后面即在执行到内存屏障这句指令时在它前面的操作已经全部完成
2它会强制将对缓存的修改操作立即写入主存
3如果是写操作它会导致其他CPU中对应的缓存行无效。
八、单例模式的双重锁为什么要加volatile
public class TestInstance{private volatile static TestInstance instance;public static TestInstance getInstance(){ //1if(instance null){ //2synchronized(TestInstance.class){ //3if(instance null){ //4instance new TestInstance(); //5}}}return instance; //6}
}需要volatile关键字的原因是在并发情况下如果没有volatile关键字在第5行会出现问题。instance new TestInstance();可以分解为3行伪代码
a. memory allocate() //分配内存b. ctorInstanc(memory) //初始化对象c. instance memory //设置instance指向刚分配的地址上面的代码在编译运行时可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序即先执行了a和c没有执行b。那么由于A线程执行了c导致instance指向了一段地址所以B线程判断instance不为null会直接跳到第6行并返回一个未初始化的对象。