volatile
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
-
保证可见性、不保证原子性;
-
禁止指令重排序。
当一条线程修改了这个变量的值,新值对于其他线程是可以立即得知的。而普通变量做不到这一点。
普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。
原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
多线程环境下,一个线程对共享变量的操作对其他线程是不可见的
volatile、synchronize和锁保证可见性
有序性
即程序执行的顺序按照代码的先后顺序执行。
内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。
为什么使用volatile?
在某些情况下,volatile同步机制的性能要优于锁(synchronized关键字),但是由于虚拟机对锁实行的许多消除和优化,所以并不是很快。
volatile变量读操作的性能消耗与普通变量几乎没有差别,但是写操作则可能慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
使用它必须满足如下两个条件:
-
对变量的写操作不依赖当前值;
-
该变量没有包含在具有其他变量的不变式中。
如何实现线程安全?
虚拟机提供了同步和锁机制
阻塞同步(互斥同步)
Java中最基本的同步手段就是synchronized关键字,其编译后会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。
在执行monitorenter指令时,首先要尝试获取对象的锁。
如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexit指令时将锁计数器-1。当计数器为0时,锁就被释放了。
如果获取对象失败了,那当前线程就要阻塞等待,知道对象锁被另外一个线程释放为止。
重入锁(ReentrantLock)
java.util.concurrent包中的ReentrantLock比synchronized增加了高级功能:等待可中断、可实现公平锁、锁可以绑定多个条件。
-
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
-
公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized中的锁是非公平的。
非阻塞同步
互斥同步最大的问题,就是进行线程阻塞和唤醒所带来的性能问题,是一种悲观的并发策略。
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。