多线程编程经常会遇到很多问题,那么这些问题可能是由什么导致的呢?
数据存在共享
如果线程访问的都是只在线程里有效的数据,那么多线程不会造成什么问题;但是如果线程操作到线程外的数据,并且这些数据别的线程也访问和操作到,那么就可能存在问题了.比如
/**
* 共享变量
*/
private static int share = 0;
public static void main(String[] args) throws Exception{
//用100个线程,每个线程 + 1000次,理论上最终结果 应该是 100000
for(int i = 0; i < 100; i ++){
new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0; j < 1000; j ++){
share ++;
}
}
}).start();
}
//暂停5秒,保证线程都能执行完毕
Thread.sleep(5000);
//最后输出的结果往往都没有达到100000的数
System.out.println("i = " + share);
}
在工作开发中,数据共享导致的多线程问题更是随处可见,比如现在没有做什么并发措施,A,B两个人几乎同时向一个手机账户(手机余额0元)里面充了50块钱话费,在代码中,是启动了a,b两个线程,然后线程进行以下操作 :
- 先从数据库里取手机账户信息
- 然后手机账户 + 50块
- 最后再写回数据库.
假设现在a线程还没把充值好的手机余额(50元)写回数据库,b线程已经从数据库里面取手机余额(0元),a写回数据库是50元,b操作完再写回数据库也是50元,而不是我们想要的100元.
资源不互斥
共享资源(可能是代码,也可能是变量)在同一时间不允许多个线程对其操作,这便是多线程的互斥性.上面充话费失败的例子便是资源不互斥导致的.假如我们线程a 在执行1,2,3任何一个操作的时候,其他线程都不能执行这段代码,a执行完这段代码,其他线程方可执行这段代码,那么这样子就不会出现充值不正确的情况,而要达到这样的效果,我们可以给1,2,3操作加一个锁,这样子代码1,2,3便具有了互斥性.
数据不可见
在Java中,假设我们要操作这样一段代码,都发生了些什么呢?
private static int i = 0;
public static void main(String[] args) {
i = 1;
}
首先,这个 成员变量i 是存储在主内存中(比如堆里面)的,当我们执行了 i = 1以后,主线程会先把这个i从主内存中copy到主线程的本地内存中,(假如现在有多个线程执行了 i = 2或者 i = 3,那么这些线程都是把变量copy到线程各自的本地内存中,然后线程对本地内存中的变量进行修改,)然后再不定时的刷回到主内存中,线程为什么要这么大费周章呢?原来如果线程直接去修改主内存的数据会很慢,这个本地内存相当于一个高速缓存的作用.这个本地内存只对本线程可见, 其他线程是无法修改改本地内存的.这就是不可见性.那有没有办法证明这一现象呢?请看下面代码:
//一个控制变量
static boolean flag = false;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
//当 flag = false 的时候,一直死循环; 当 false = true的时候跳出循环
while (!flag) {}
System.out.println(Thread.currentThread().getName());
}
}.start();
}
//休眠1秒钟,保证上面的线程都已经执行了
Thread.sleep(1000);
new Thread() {
@Override
public void run() {
//修改控制变量为true,想让上面的线程都跳出循环,输出线程号
flag = true;
}
}.start();
Thread.sleep(1000);
new Thread() {
@Override
public void run() {
//一般输出 flag = true
System.out.println("flag = " + flag);
}
}.start();
}
上面的代码多执行几次后,我们会发现有死循环的现象,出现死循环是因为其他线程的flag还是等于false,说明读取的还是本地内存.哪有什么办法来破除这一现象吗?那就是volatile.
volatile运用了缓存一致性的原理,被修饰了volatile的共享变量当被修改后,系统会通过MESI协议通知其他线程里的本地内存里的该共享变量,令其失效,当线程重新读取这一变量的时候,就不是从本地内存里取了,而是去主存里取.在上面的代码中,如果我们把上面的代码改为
static volatile boolean flag = false;
便不会出现死循环的情况.
操作不是原子操作
原子性是指一个操作或多个操作要么全部执行,且执行的过程不会被线程调度器打断,要么就都不执行。像我们的i++就不是原子操作,因为 i++ 包括 读取 i , i + 1 等于啥, 写回 i 等几个步骤,cpu执行到i + 1的时候可能先去执行其他线程,然后再回来执行这个,在java中,对long,double的操作可能不是原子操作,因为long,double是64位数据类型,当我们的虚拟机为32位的时候,对long的操作其实是分两步操作的,高位处理和低位处理.
Java中的原子操作
lock:将一个变量标识为被一个线程独占状态
unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定
read:将一个变量的值从主内存传输到工作内存中,以便随后的load操作
load:把read操作从主内存中得到的变量值放入工作内存的变量的副本中
use:把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令
assign:把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时,都要使用该操作
store:把工作内存中的一个变量的值传递给主内存,以便随后的write操作
write:把store操作从工作内存中得到的变量的值写到主内存中的变量
CAS
CAS(compare and swap)是操作系统的一条指令,作用为当我们要修改内存中value的值,我们可以通过比较value与expect的关系,再决定是否要把newValue写进去
- 当value == expect 时, 说明内存中的value还没被修改过,可以把newValue写进去,并返回true
- 当value != expect时, 说明内存中的value已经被修改过了,并返回false
假如现在内存上有一个变量 n = 10, 有A,B两个CPU同时想通过CAS修改n的值, Acpu的expect = 10, newValue= 11;Bcpu的except = 10, newValue = 12,假设Acpu先取的n的修改权,这时候会锁定内存n,B就无法同时访问n了,必须等A执行完毕,A的except = value = 10,所以可以执行value= newValue,被修改过后的n的值为11,A执行完以后就轮到B了,B比较except = 10,而此刻n = 11,两个不相同,所以执行失败.
那么在java中是否有对应的方法直接使用了CAS这条指令呢?那就是Unsafe这个类的
public final native boolean compareAndSwapObject(Object object, long offset, Object expect, Object newValue);
其中object是指某个对象,offset是指对象的某个字段的偏移量,cas就是通过这个偏移量去找到内存中对象内对应的字段的位置,expect表示期待值,newValue表示新值,如果返回true,表示object这个对象偏移了offset的字段filed已经被修改成expect了,反之则失败.
怎么用CAS安全的自增
//一个普通的对象,这个对象里面有个value的字段,注意这个value字段是volatile
final CAS cas = new CAS();
//unsafe的初始化代码可以在网上找
final Unsafe unsafe = getUnsafe();
//获取value字段在对象里面的偏移量
final long offset = getOffset(unsafe);
for(int i = 0; i < 10000; i ++){
new Thread(){
@Override
public void run(){
for(int j = 0; j < 10000; j ++){
int v ;
do{
//获取value的值
v = cas.getValue();
//做cas操作,如果失败的话重新获取value的值然后再做cas操作,直至成功
}while (!unsafe.compareAndSwapInt(cas, offset, v, v + 1));
}
}
}.start();
}
//保证所有的线程都能执行完
Thread.sleep(20000);
//最后正确的输出value:100000000
System.out.println("value : " +cas.getValue());
CPU运行时指令重排序
程序执行的顺序并不一定按照代码的先后顺序执行,比如现在有一段代码:
int i = 0;
int j = 1;
i = 10; //语句1
j = 100;//语句2
在真正代码执行过程中,语句1并不一定比语句2先执行,cpu会根据代码的实际情况优化代码的执行顺序..所以你看到的可能是语句2比较先执行,但是调整过顺序的执行顺序最后一定会跟没有调整过执行顺序的结果是一致的..
在单线程中,调整顺序可能不会造成什么影响,但是多线程的话就不一定了..比如:
boolean a = false;
Context c = null;
//线程 1
c = initContext();
a = true;
//线程2
while(!a){
sleep();
}
dosomething(context);
我们本来的打算是 同时开启两个线程,线程1初始化容器,初始化完容器以后把a的状态置为 true..线程2先判断 a的值,在本来的打算中,因为线程1的容器初始完以后才会改a的状态,也就是说当a的状态是false的时候,说明c还没初始完,c没初始完就不能执行dosomething,会报空指针异常.所以我们sleep一下给线程1充足的的时间去初始化容器..但是现在如果线程1 被cpu调整了执行顺序,即先把a=true先执行了...那么线程2会以为容器c已经初始化完了..从而执行something 从而报错..除了用锁可以解除这种重排序的困扰,还可以用volatile.