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对象可以同时绑定对个对象。
- 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;