1、volatile是轻量级的synchronized,他在多处理器开发中保证了共享变量的“可见性”(和有序性)。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile使用恰当的话,他会比synchronized执行成本更低,因为他不会引起线程上下文的切换和调度。
2、有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码。
意味着如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
原子性:指一个操作是不可中断的。即使是多个线程在一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
比如,对于一个静态全局变量int i, 两个线程同时对它赋值,线程A给它赋值1,线程B给它赋值为-1。那么不管这两个线程以何种方式、何种步调工作,i的值要么是1,要么是-1。线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。
(但long类型读写不是原子性的)
可见性:当一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改。
缓存优化和硬件优化、指令重排及编译器的优化都可能导致可见性问题
有序性:在并发时,程序的执行可能会出现乱序。有序性问题的原因在于程序执行时,可能会发生指令重排。
(指令重排不会使串行的语义逻辑发生问题)
不能重排的指令:
●程序顺序原则:一个线程内保证语义的串行性。
●volatile规则: volatile变量的写先于读发生,这保证了volatile变量的可见性。
●锁规则:解锁(unlock)必然发生在随后的加锁(lock) 前。
●传递性:A先于B,B先于C,那么A必然先于C。
●线程的start()方法先于它的每一 个动作。.
●线程的所有 操作先于线程的终结(Thread.join()) 。
●线程的中断(interrupt() 先于被中断线程的代码。
Lock前缀的指令在多核处理器下回引发两件事情:
1)将当前处理器缓存行的数据写回到系统内存
2)这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
volatile具有可见性和有序性但不保证原子性。
volatile对保证操作的原子性有很大的帮助,但volatile并不能替代锁,他也无法保证一些复合操作的原子性,比如i++
Volatile:
当把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其它内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型变量时总会返回最新的值。
也就是说volatile类型的变量保证了可见性 但是不能保证原子性 在进行自增等非原子性操作的时候依然会出现并发问题。
Volatile和锁:
当多个线程同时请求锁的时候,一些线程将被挂起并且等待其他线程执行完它们的时间片后才能被调度执行。频繁的线程间上下文切换及线程调度是十分耗资源的。另外锁还存在着死锁的风险。
与锁相比,volatile是一种更加轻量级的同步机制,因为在使用这些变量的时候不会发生上下文切换和线程调度等操作。但是volatile同样也存在局限性:当变量依赖于其他变量或旧值时(自增)就不能使用volatile变量,因为他们不是原子操作。
synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢? 在上面一篇文章中,我们了解到如果一个代码块被synchronized修饰了 ,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况: 1 )获取锁的线程执行完了该代码块,然后线程释放对锁的占有; 2 )线程执行发生异常,此时JVM会让线程自动释放锁。 那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁 ,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。 1.因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者 能够响应中断) , 通过Lock就可以办到 . 2.再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。 但是采用synchronized关键字来实现同步的话,就会导致一个问题: 如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。 因此就需要一种机制来使得多 个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。 3.另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下,也就是说L ock提供了比synchronized更多的功能。但是要注意以下几点: 1 ) Lock不是Java语言内置的, synchronized是Java语言的关键字,因此是内置特性。Lock是一 个类 ,通过这个类可以实现同步访问; 2 ) Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后, 系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
在锁层次上具体说明
1.Synchronized Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时 ,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1 ,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一 个线程释放为
2.ReentrantL ock 由于ReentrantL ock是java. util.concurrent包下提供的一套互斥锁,相比Synchronized , Reentrantl _ock类提供了一些高级功能,主要有以下3项: 1等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。 2.公平锁,多个线程等待同-个锁时,必须按照申请锁的时间顺序获得锁, Synchronized锁非公平锁, Reentrantl .ock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。 3.锁绑定多个条件,一个Reentrantl ock对象可以同时绑定对个对象。
2、可见性 对于可见性, Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。通过synchronized和L ock也能够保证可见性, synchronized和Lock能保证同- 时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3、有序性 在Java里面,可以通过volatile关键字来保证一定的“有序性”。 看不懂就是说禁止指令重排序 Java内存模型具备一-些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before原则。如果两个操作的执行次序无法从happens- before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。 下面就来具体介绍下happens-before原则(先行发生原则) : ● 程序次序规则:一个线程内,按照代码顺序, 书写在前面的操作先行发生于书写在后面的操 作 ●锁定规则:一个unLock操作先行发生于后面对同一一个锁额lock操作 ●volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作 ● 传递规则:如果操作A先行发生于操作B ,而操作B又先行发生于操作C ,则可以得出操作A先 行发生于操作C ●线程启动规则: Thread对象的start()方法先行发生于此线程的每个-个动作 ●线程中断规则 :对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件 的发生 ●线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread. join()方法结束、Thread isAlive()的返回值手段检测到线程已经终止执行 ●对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
volatile是一个轻量级的同步机制。用来修饰共享可变变量,对volatile变量的读写操作都是从高速缓存或者主内存中读取。
volatile的作用
volatile关键字被称为轻量级锁,能保证可见性和有序性。不同的是原子性方面仅能保证写volatile变量操作的原子性,但没有锁的排他性。而且不会引起上下文的切换。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 第一层: ①保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 ②禁止进行指令重排序。 第二层: ①强制将修改的值立即写入主存; ②会使共享变量的缓存无效,只能去主存中读取数据
volatile的原理和实现机制
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏) , 内存屏障会提供3个功能:| 1 )它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; 2)它会强制将对缓存的修改操作立即写入主存; 3)如果是写操作,它会导致其他CPU中对应的缓存行无效。 总结: volatile的作用: 1、对于共享变量,每个线程对其的缓存失效 2、能够保证每个线程对变量做到可见性,一个线程修改,其他线程都能看见(强制将修改值压入内存) 3、禁止指令重排,但无法做到原子性,还要依赖其他锁机制
synchronized关键字
synchronized提供了两种主要特性:互斥性和可见性。
互斥性实现:互斥即一次只允许一个线程持有某个特定的锁。换句话说,如果将锁加在某个变量上,则每次只有一个线程能够使用该共享数据,直到该线程使用完才会将该共享数据释放,供其它线程使用。
可见性实现:线程在得到锁时读入副本,释放时写回内存。
volatile关键字
volatile修饰的变量具有可见性,并且局部阻止了指令重排的发生。
可见性实现:当使用volatile修饰变量时,意味着任何对此变量的操作都会在内存中进行,不会产生副本,以保证共享变量的可见性。
禁止指令重排:CPU为了提高程序指令的执行效率,会对输入的代码进行指令优化,分析哪些取数据动作可以合并进行和哪些存数据动作可以合并进行。它不保证各语句的执行顺序与代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排不会影响单个线程的执行,但是会影响到多个线程并发执行的正确性。volatile会限制编译器对修饰变量的相关读写操作和指令重排,确保在volatile之前的操作已经完成,在volatile之后的操作还未开始。
总结
volatile仅能使用在变量上,synchronized可以使用在变量和方法上;
volatile仅能实现变量的可见性,不能保证原子性,synchronized可以保证变量的可见性和原子性;
volatile不会造成线程阻塞,synchronized可能会造成线程阻塞(因为volatile只是将当前变量的值及时告知所有线程,而synchronized是锁定当前变量不让其它线程访问);
volatile标记的变量不会被编译器优化(因为不能指令重排),synchronized标记的变量可以被编译器优化;
volatile修饰变量适合于一写多读的并发场景,而多写场景一定会产生线程安全问题(因此使用volatile而不是synchronized的唯一安全情况是类中只有一个可变的域)。
因为所有的操作都需要同步给内存变量,所以volatile一定会使线程的执行速度变慢。
有序性很简单,就是内存屏障,禁止指令重排序。可见性一般来说我会结合缓存一致性协议来说,MESI,
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
不具备,
会
原文:https://www.cnblogs.com/jokerq/p/11190667.html