现在的工作中,多线程肯定是经常挂嘴边的,实际上我们在做项目过程中,去写多线程的情况其实没多少,一般这些操作都是被封装在框架里,所以不会多线程的技术不会影响工作,但是如果多少懂一些原理会有助于理解代码.
使用锁
一说起并发,大家第一想到的肯定是锁,加锁确实能够很好的保证数据一致性,因为一段代码加锁后,只有获取了锁的线程才能执行该代码块,等到代码块执行完了之后并执行完成释放锁的操作后,其他的线程就可以去竞争这个锁,得到锁的线程可以执行该代码块,而没有竞争到锁的线程只能等待.
看一段没有锁的代码(使用java代码演示)
public class LockDemo {
public static int num = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
doSomethings();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads.add(t);
t.start();
}
//* 等待所有线程执行完毕
for (Thread t : threads) {
t.join();
}
System.out.println("num =" + num);
}
public static void doSomethings() throws InterruptedException {
System.out.println("累加前num的值为" + num);
num++;
Thread.sleep(1);//加一个sleep防止编译器优化,2条语句可以被优化成num += 2;
num++;
System.out.println("累加后num的值为" + num);
}
}
这段代码的意思是用100个线程取累加全局变量num,正常情况下num最后的值应该为200,但是结果却很意外,看console:
...
...
累加后num的值为191
累加后num的值为190
累加后num的值为192
累加后num的值为193
累加后num的值为194
累加后num的值为196
累加后num的值为195
累加后num的值为197
num =197
最后num并不是等于200,而是等于197,并且每次执行完成,num的值都不一样.
因为线程读取到的值并不一定最新的值,例如条线A程拿到num的时候是100,但是线程B很快就更新了num,使num变成了101,但是线程A还是拿100++,所以最后又把num设置成了101,这样有一次计算就白做了.
如何解决这个问题呢,最简单的做法就是加锁
加锁之后的代码
public static void doSomethings() throws InterruptedException {
synchronized (LockDemo.class) {
System.out.println("累加前num的值为" + num);
num++;
Thread.sleep(1);//加一个sleep防止编译器优化,2条语句可以被优化成num += 2;
num++;
System.out.println("累加后num的值为" + num);
}
}
这里用了synchronized
关键字,这个关键字可以让同一时刻,只有一个线程执行代码块,所以很好的解决了数据一致性问题,加了synchronized之后的console:
...
...
累加后num的值为196
累加前num的值为196
累加后num的值为198
累加前num的值为198
累加后num的值为200
num =200
使用reentrantLock
java中也可以使用reentrantLock对象来执行加锁操作(推荐)
定义一个成员对象:
private static ReentrantLock reentrantLock = new ReentrantLock();
在需要加锁的地方使用lock()和unlock()
public static void doSomethings() throws InterruptedException {
reentrantLock.lock();
System.out.println("累加前num的值为" + num);
num++;
Thread.sleep(1);//加一个sleep防止编译器优化,2条语句可以被优化成num += 2;
num++;
System.out.println("累加后num的值为" + num);
reentrantLock.unlock();
}
volatile
可能有的人觉得用锁太消耗资源,实时是这样的,因为加锁之后,底层实现上会加总线锁的操作,这种情况下,cpu不能访问内存,所以其他线程不能做任何操作.所以说加锁代价太大,所以这里volatile可以避免部分加锁操作
这里说的是部分,也就是说volatile并不能代替加锁,因为volatile修饰的变量相当于每次都去内存取,而不会在缓存中去,我们知道cpu有高速缓存,就是优化一些经常访问的数据.但是在这个例子中,线程取了num的值,就算取的一瞬间num值是最新的,但是有可能,刚取完,这个线程的被分配的时间片用完了,过了很久这个线程才重新被唤醒,然后执行累加操作,这个时间就尴尬了...
加了volatile修饰之后的结果
累加后num的值为194
累加后num的值为195
累加后num的值为196
累加后num的值为197
累加后num的值为198
累加后num的值为199
num =199
我执行了好几遍,发部分都是返回200,但是还是存在不是200的情况,如上,也就是说volatile治标不治本.
CAS
用锁也不好,不用锁也不好,那怎么办啊,难道使用多线程必须付出很大的代价吗,其实不是,还有一个非常好的方法,不用加锁都能做到数据一致性,也就是CAS算法,什么是CAS呢,举个例子:
如果有A,B,C这3个线程同时操作num,假如num如果现在是100,那么A线程,B线程,C线程都读取了num,那A,B,C都是持有100这个数字,这时B线程做了累加操作,然后修改了num为100,在修改的时候做一个CAS的操作,也就是比较一下num的值,比较什么东西呢,B在修改num的时候需要比较num是不是100,因为B是把100改成了101,所以如果B碰到了num不是100的情况那就是有问题了,如果num当前不是100,B强行把num改成了101,那最后结果肯定有问题.因为中间某些操作被B给掩盖了.碰到问题了怎么办呢,最好的办法就是丢弃这次操作,然后重新读取num的值做累加操作.
代码实现
因为CAS是需要CPU的机器指令支持的,所以代码上我用一个锁来模拟这个指令
public static void doSomethings() throws InterruptedException {
System.out.println("累加前num的值为" + num);
int olnNum = num;
int newNum = num + 1;
//* 模拟cas,并做2遍操作,因为一边操作意外重现率太低,所以这里为了演示做2遍
boolean loop = true;
while (loop) {
int currentNum = num;
synchronized (LockDemo.class) {
if (olnNum == currentNum) {
num = newNum;
loop = false;
}
}
//** 失败后重新读取
if (loop) {
System.out.println("currentNum = " + currentNum + ",但是oldNum = " + olnNum);
olnNum = num;
newNum = num + 1;
}
}
//* 第二遍
loop = true;
while (loop) {
int currentNum = num;
synchronized (LockDemo.class) {
if (olnNum == currentNum) {
num = newNum;
loop = false;
}
}
//** 失败后重新读取
if (loop) {
System.out.println("currentNum = " + currentNum + ",但是oldNum = " + olnNum);
olnNum = num;
newNum = num + 1;
}
}
System.out.println("累加后num的值为" + num);
}
console
累加后num的值为197
累加后num的值为164
currentNum = 162,但是oldNum = 161
currentNum = 161,但是oldNum = 160
累加后num的值为199
累加后num的值为157
累加后num的值为156
currentNum = 151,但是oldNum = 150
累加后num的值为200
累加后num的值为198
累加后num的值为196
累加后num的值为193
累加后num的值为191
累加后num的值为190
累加后num的值为187
num =200
tip
有时候光比较值也是不够的,比如比较之后发现old和预期的一样,但是实际上中途有一个加操作和减操作,导致old和预期的一样,所以实际上在比较的时候应该额外加一个版本号的比较,版本号能够体现数据是否中途有被更新过,比如每次更新之后版本号+1,这样可以保证比较的时候是不是预期的版本
上面就是对CAS一个用户态的实现,仅仅是一个模型而已,方便大家理解.
网友评论