1. 并发编程中的三个概念
(1) 原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行.
分析下面哪些操作是原子操作:
① y=1;
② y=x;
③ y++;
结果是只有①是原子操作
y=x, 分两步, 第一步取 x 得值, 第二步把这个值赋值给 y
y++ 其实是 y=y+1; 该操作有三步, 第一步取 y 得值, 第二步把值 + 1, 第三步重新赋值
从上面可以分析出只有简单读值或者写值才是原子操作
(2) 可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值.
举例:
比如
//线程1执行代码
int a = 1;
a = 10;
//线程2执行代码
b = a++;
线程 1 执行的过程: 先从内存读取 a 的值, 然后再给 a 重新赋值为 10. 这个时候线程 2 执行了, 线程 2 在内存读取到 a 的值是 1, 因为这个时候线程 1 可能没有把 a 更新后的值重新写入内存, 这就造成了不可见性
(3) 有序性
即程序执行的顺序按照代码的先后顺序执行
举例:
int a = 0;
int b = 0;
a = 1; //语句1
b = 2; //语句2
从代码顺序上看,语句 1 是在语句 2 前面的,那么 JVM 在真正执行这段代码的时候会保证语句 1 一定会在语句 2 前面执行吗?不一定,为什么呢?这里可能会发生指令重排序.
指令重排序: 一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的.
比如上面的代码中,语句 1 和语句 2 谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句 2 先执行而语句 1 后执行.
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句 1 和语句 2 没有数据依赖性,因此可能会被重排序.假如发生了重排序,在线程 1 执行过程中先执行语句 2,而此是线程 2 会以为初始化工作已经完成,那么就会跳出 while 循环,去执行 doSomethingwithconfig(context) 方法,而此时 context 并没有被初始化,就会导致程序出错.
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性.
2.volatile 可以保证可见性和有序性, 不能保证原子性
(1) 如何保证可见性?
volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的.
举例:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
上面这段代码在多数情况下是可以保证能退出 while 循环的, 但是也有可能不会. 比如线程 2 在给 stop 赋值之后没有马上把 stop 的值写入内存而是去干别的事了, 这个时候 stop 值还是原来的, while 循环就不会退出.
但是如果 stop 变量用 volatile 修饰之后就变得不一样了:
使用 volatile 关键字会强制将修改的值立即写入主存, 也就是线程 2 在修改完 stop 的值之后会把 stop 的值立即写入内存;
使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程 1 的工作内存中缓存变量 stop 的缓存行无效, 由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1 再次读取变量 stop 的值时会去主存读取.
(2) 如何保证有序性?
volatile 关键字能禁止指令重排序
volatile 关键字禁止指令重排序有两层意思:
当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行.
举个简单的例子
//x,y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于 flag 变量为 volatile 变量,那么在进行指令重排序的过程的时候,不会将语句 3 放到语句 1,语句 2 前面,也不会讲语句 3 放到语句 4,语句 5 后面.但是要注意语句 1 和语句 2 的顺序,语句 4 和语句 5 的顺序是不作任何保证的.
(3) 为什么不能保证原子性
举个例子:
public class Test {
public volatile int x = 0;
public void increase() {
x++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
假设执行到某一个时段, x 的值为 10
线程 1 执行 x++,x 为 10,x++ 相当于 x=10+1,已经使用变量 x 值算出自增后的值 11,但是 11 未付值给 X, 也就是没有写入主内存,然后线程 2 从内存中读取 x 为 10,进行自增后为 11 并写入主内存,这个时候线程 1 中的 x 已经失效,但是将要给 x 复制的值 11 已经算出来了,不再需要 x 这个变量了,失效不失效无所谓了,完成 x=11 后写入主内存. 结果就是两次线程的操作只给 x 增加了 1, 因此从这个例子可以看出 volatile 不能保证原子性
3.ReentrantLock ,synchronized 可保证原子性, 可见性
ReentrantLock ,synchronized 都可以实现多线程编程的安全性.
相同点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的
不同点:
这两种方式最大区别就是对于 Synchronized 来说,它是 java 语言的关键字,是原生语法层面的互斥,需要 jvm 实现.而 ReentrantLock 它是 JDK 1.5 之后提供的 API 层面的互斥锁,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成.
ReentrantLock 相比 synchronized 的高级功能:
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized 来说可以避免出现死锁的情况.
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized 锁非公平锁,ReentrantLock 默认的构造函数是创建的非公平锁,可以通过参数 true 设为公平锁,但公平锁表现的性能不是很好.
锁绑定多个条件,一个 ReentrantLock 对象可以同时绑定对个对象.
来源: http://www.jianshu.com/p/94ae2257c747