有时候仅仅为了一个或者两个的实例域就是用synchronized的话,开销就会很大,而Java为我们提供了另一种同步的免锁机制,volatile。Volatile可以看成是synchronized的轻量级,功能也仅仅是synchronized的一部分,认识volatile之前,先认识Java内存模型和Java的原子性,可见性,有序性
Java的内存模型
在Java虚拟机中,每一个线程私有的就是JVMstack ,本地方法栈,PC,而共享的就是Java堆和方法区(包含常量池)。Java创建的对象都在堆中分配,所有的实例域,静态变量,数组的元素都在堆中,都是被线程共享的。
而Java线程之间的通信是通过共享内存来实现的,也就是隐式通信,而线程通信是由Java内存模型控制的(JMM),JMM决定着一个线程对一个共享变量的写入修改何时对另一个线程可见(也就是另一个线程得到那个最新的修改值),JMM定义了线程与主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程在自己的栈中(栈中有栈帧)都有一个私有的本地内存空间,其他线程不可访问,本地内存保存着这个共享变量的拷贝。线程不能直接对共享变量直接写,先写入本地内存,然后由JMM决定什么时候将本地的值回写打主存中。
本地内存是JMM的一个抽象概念,并不真实存在,抽象示意图:
先看个例子:
public class
Main {
public staticvoid main(String[] args) {
Test test = new Test(5);
MyThread myThread =new
MyThread(test);
MyRunnablemyRunnable = new
MyRunnable(test);
Thread thread = new
Thread(myRunnable);
myThread.setName("MyThread");
thread.setName("MyRunnable");
thread.start();
myThread.start();
}
}
public class
Test {
public int test;
public Test(int
test) {
this.test
= test;
}
}
public class
MyThread extends
Thread {
Test test;
public MyThread(Test test) {
this.test
= test;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "
+test.test);
test.test
= 10;
try {
System.out.println(Thread.currentThread().getName()+" test.test= 10;");
System.out.println(Thread.currentThread().getName()+"
睡觉");
Thread.currentThread().sleep(2000);
System.out.println(Thread.currentThread().getName()+" "
+test.test);
} catch
(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" "
+test.test);
}
}
public class
MyRunnable implements
Runnable {
Test test;
public MyRunnable(Test test) {
this.test
= test;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "
+test.test);
test.test
= 20;
// try {
System.out.println(Thread.currentThread().getName()+" test.test= 20;");
System.out.println(Thread.currentThread().getName()+""+test.test);
}
}
输出:(这个输出随机性很大,运行很多次才偶然得到)
MyRunnable 5
MyRunnable test.test = 20;
MyRunnable 20
MyThread 5
MyThread test.test = 10;
MyThread 睡觉
MyThread 10
MyThread 10
结果说明:在main线程创建了线程MyThread和MyRunnable两个线程,实例test的域原始值是5。由输出可以知道,MyRunnable线程先执行完之后MyThread线程才开始执行,可以看到,在MyRunnable线程中,我们把test对象的域改为了20,但是在MyThread线程中打印出来的值还是5,要么是MyRunnable的值没写回内存,要么就是MyThread的本地值是一开始test域没改之前的值(没更新MyRunnable改过的值)。这就很好的印证了上面那幅图
原子性
如果学过操作系统或者数据库都会有这样的概念,在生活中,A转100给B执行的语句就是
A = A -100;
B= B+100;
那么在数据库写入的时候就会用事务管理来操作,保证其原子操作,也就是害怕执行了 A = A -100;
突然故障导致不能往下执行,那么A就会无端端少了100,而B也没有得到这100,原子操作就是要么执行完全部,要么全部不执行,在数据库中,事务管理器针对上面的情况就会恢复A的值,也就是让A重新+100,恢复以前的值。Java的原子性也就是差不多的意思。
例子:
①X = 100;
②Y=X +100;
① 就是原子操作,对基本数据类型的写入和读取都是原子性操作,即这些操作要么不执行,要么执行,不能被打断
② 而就不是原子操作了,因为涉及到X的读取,X+100后赋值给Y,虽然这两个分开就是原子操作,但是结合一起就不是原子操作了,这两个过程可能被打断
原子就是不能再分的最小的例子,所以对应操作也是不能再分和打断
java.util.concurrent.atomic包含AtomicBoolean,AtomicLong和AtomicReference这些原子类仅供开发并发工具的系统程序员使用,应用程序员不应该使用这些类。
可见性
其实可见性在上面的代码例子中已经可以体现,意思就是线程1修改了共享变量的值之后,其他的线程可以知道这个变量值已经修改,并且能够得到这个新值
有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性,更多的了解可以看这里
重排序并不是Java特有的,在操作系统也是会有的,为了性能优化而重排序是很正常的
Volatile关键字
当一个变量定义为volatile之后,它具备两项特性
①可见性
②禁止指令重排序优化
可见性demo:
public class Main { public static void main(String[] args) throws InterruptedException { MyThread myThread = new MyThread(); myThread.start(); Thread.currentThread().sleep(1000); System.out.println("sleep 完成"); myThread.setFlag(true); }}
public class MyThread extends Thread { boolean flag = false; public void setFlag(boolean flag) { this.flag = flag; } @Override public void run() { while (!flag) { } } }
结果:
结果说明:
每个线程都有自己的工作(本地内存)内存,myThread线程读取了flag的值到自己的工作内存中,拷贝了一份,然后一直进入死循环,而当main线程也有自己的工作内存,它拿到了flag的值,改变了flag的值,但是或许还没来得及将它写会到住内存中就已经结束了,而myThread一直不知道flag的值已经改变,所以一直循环下去
加上volatile关键字
volatile boolean flag = false;
① 使用volatile修饰的变量,当变量改变之后,会立马强制写会内存
② 当某个线程修改了这个变量,其他线程使用前,会直接从内存中读取一次
Volatile不具有原子性(一般)
public class Main { volatile static int flag = 0; public static void increment() { flag++; } public static void main(String[] args) throws InterruptedException { final Main test = new Main(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increment(); }; }.start(); } //保证前面的线程都执行完 while(Thread.activeCount()>2) Thread.yield(); System.out.println(test.flag); } }
这里是jdk1.8所以while(Thread.activeCount()>2)如果还是1.7的话就改为大于1
输出的结果大多数情况下都是小于10000,因为自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行。
假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于已经读取了inc的值(不会再次去主存读数据),注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,inc只增加了1。
自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
正确使用 volatile 变量的条件
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
· 对变量的写操作不依赖于当前值。
· 该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使 volatile
变量不能用作线程安全计数器。虽然增量操作(x++
)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile
不能提供必须的原子特性。实现正确的操作需要使 x
的值在操作期间保持不变,而
volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile
变量不能像 synchronized
那样普遍适用于实现线程安全。清单
1 显示了一个非线程安全的数值范围类。它包含了一个不变式 ——
下界总是小于或等于上界。
参考:
http://www.jianshu.com/p/4377b3245a2c
http://www.ibm.com/developerworks/cn/java/j-jtp06197.html