对于关键字synchronized,volatile并不是太陌生,在多线程中会处理同步(数据争用)问题,下面简单的介绍下这两种关键字的用法。该内容摘自慕课网上MartonZhang老师课程,http://www.imooc.com/learn/352。
synchronized:可以实现共享变量可见性,并且是原子性操作(同步),是同步锁
volatile:可以实现共享变量可见性,但不是原子性操作,不需要加锁,比synchronized更轻量,不会阻塞线程
什么是可见性呢?
可见性:通俗的讲一个线程对共享变量值得修改,能够及时地被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都有副本,那么这个变量就是这几个线程的共享变量
理解工作内存以及共享变量首先简单了解java内存模型,JMM(Java Memory Model):
描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节
比如:
1.所有的变量都存储在主内存中
2.每个线程都有自己独立的工作内存,里面保存该线程使用到的变量副本(主内存中该变量的一份拷贝)
比如在主内存中有一个变量x,那么在线程1,2,3工作内存中都会有一份x的拷贝值,其中x称为共享变量。注意:
1.线程对共享变量的所有操作都必须在自己的工作内存中运行,不能直接从主内存中读取
2.不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值得传递需要通过主内存来完成
比如线程1对x进行修改,如果让线程2能及时的看到线程1修改后的值,那么需要把线程1工作内存中的x值刷新到主内存中,然后再把主内存中x的值刷到线程2中的工作内存中,那么线程2中x的值就是线程1修改后的值了。
实现可见性:
1.线程修改后的共享变量能够及时从工作内存中刷新到主内存中
2.其他线程能够及时把共享变量中的最新值从主内存中更新到自己的工作内存中
Java中实现共享变量在线程间可见性的方法(Java语言层面):
synchronized
volatile
synchronized
JMM中关于synchronized的两条规定:
1.线程解锁前,必须把共享变量的最新值刷新到主内存中
2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取到最新值(注意:加锁以及解锁需要是同一把锁)
线程解锁前对共享变量的修改在下次加锁时对其他线程可见
线程执行互斥代码的过程:
1.获得互斥锁
2.清空内存
3.从主内存中拷贝变量的最新副本到工作内存中
4.执行代码
5.将更改后的共享变量的值刷新到主内存中
6.释放互斥锁
重排序:
代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化
1.编译器优化的重排序(编译器优化)、
2.指令级并行重排序(处理器优化)
3.内存系统的重排序(处理器优化)
简单的示例:
书写顺序: 可能的执行顺序:
int number = 1; int result = 0;
int result = 0 int number = 1;
as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java编译器,运行时和处理器都会保证Java在单线程下遵循as-if-serial语义),在单线程中程序执行是遵循as-if-serial语义的:
int a = 1; //第一行
int b = 2; //第二行
int c = a + b; //第三行
单线程:无论a,b如何重排序,但是第三行代码的顺序不会变,总是等于a+b的值,重排序不会给单线程带来内存可见性的问题
多线程中程序交错执行时,重排序可能会造成内存可见性问题
导致共享变量在线程间不可见的原因:
1.线程的交叉执行
2.重排序结合线程交叉执行
3.共享变量更新后的值没有在工作内存与主内存间及时更新
synchronized保证了一段时间内只有一个线程执行当前代码块,当前锁释放后,别的线程才可以执行。
volatile
volatile实现可见性
深入来说:通过加入内存屏障和禁止重排序优化来实现
1.对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
2.对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存中,这样任何时刻,不同的线程总能看到该变量的最新值。
线程写volatile变量的过程:
1.改变线程工作内存中volatile变量副本的值
2.将改变后副本的值从工作内存刷新到主内存中
线程读volatile变量的过程:
1.从主内存中读取volatile变量的最新值到线程的工作内存中
2.从工作内存中读取volatile变量的副本
volatile不能保证volatile变量复合操作的原子性:
private int number = 0;
number ++; //不是原子操作
执行步骤:
1.读取number的值
2.将number的值加1
3.写入最新的number的值到内存中
如果加synchronized(this){
number++;//变为了原子操作
}
private volatile int number = 0; //不是原子操作
number++;//volatile不能保证上面执行的三个步骤的原子性
保证volatile变量操作的原子性:
1.加synchronized
2.使用ReentrantLock(java.util.concurrent.locks)
3.使用AtomicInterger(vava.util.concurrent.atomic)
在多线程中安全使用volatile变量,必须同时满足:
1.对变量的写入操作不依赖其当前值
不满足:number++ , number *=5;
2.该变量没有包含子具有其他变量的不变式中
通过以上的大概说明,下面通过代码来说明:
public class myThread extends Thread {
public void run() {
getI.geti();
}
public static void main(String [] args){
myThread thread1 = new myThread();
thread1.start();
System.out.println("thread1的Id="+thread1.getId());
myThread thread2 = new myThread();
System.out.println("thread2的Id="+thread2.getId());
thread2.start();
}
}
/**
* Created by diy_os on 2016/11/4.
*/
public class getI {
public volatile static int i =0;
public static void geti(){
System.out.println(++i+ " " +Thread.currentThread().getId());
}
}
多次执行:
可能会问,
以及用关键字volatile修饰i,说明i对所有线程都是可见的,只要i的值被修改,都会被刷到主内存中
来分析出现第一种情况的可能性:
可能线程1,正在把i从主内存中拷贝到自己的工作内存中,此时i=0;但是此时线程1的cpu时间片已经用完,此时线程1被"踢出来"(让出cpu资源,进入阻塞状态),线程2此时使用cpu资源,从主内存中读取i的值,并且将i的值加1,此时i由0变成了1,根据volatile关键字的特性,被修改后的i被刷新到主存中,此时线程2的工作内存和主存中的i值都为1;当线程2的cpu时间片使用完时,此时线程1又获得cpu资源,此时线程1的工作内存中i的值还是0,然后继续操作,把i的值加1,然后刷新到主存中,此时主存中i的值仍然是1,所以两次打印都是1.
上面的分析过程,说明volatile关键字不能保证++i的原子性,解决方法上面已经给出。