一、前文回顾
上篇文章,我们提到多线程读写共享变量时,由于多个线程同时操作主内存中的共享变量时,会把其拷贝到每个线程的工作内存中进行操作,所以会出现缓存不一致的问题。
本篇文章,我们将盘点一下常见并发问题的解决方法有哪些?这里用一张图说明:
图一
从上图我们看到,解决并发问题的方法分为两大类:无锁和有锁。
无锁可分为:局部变量,不可变对象,ThreadLocal、CAS原子类
有锁可分为:synchronized 关键字和ReentrantLock可重入锁
下面,我们分析下为什么这些方法能解决并发问题?
二、局部变量
因为局部变量仅仅存在于每个线程的工作内存中,假设有2个线程执行如下代码:
只有当每个线程执行到 : int i=0 这一行代码的时候,会在各自新城所在工作内存中创建这个变量,如下面图2所示:
图2
看到这里你就会明白,原来每个线程都只在自己的公祖哦内存操作各自变量 “i”,不同线程之间的”i”根本没有任何奇交集,所以就不存在并发问题了。
Jmeter 并发测试通过,10个线程,循环1次。全部打印 1。
三、不可变对象
所谓不可变对象是指一经创建,就对外的状态就不会改变的对象。如果一个对象的状态是亘古不变的,那么自然就不存在什么并发问题。因为对象是不可变的,所以无论多少个线程,对它做什么操作,它都是不变的。
四、ThreadLocal
ThreadLocal本质上也是在每个线程有自己的一个副本,每个线程的副本是互不影响的,没有任何关系的。
图3
如图3所示,一个命名为 “i”的ThreadLocal类,它会在每个线程都有一个Integer的 对象,虽然每个线程都会从主内存中把Integer对象拷贝到工作内存中,但是线程1和线程2拷贝过来的对象并不是同一个对象,其实每个对象只会被其中一个线程操作,这种场景不存在所谓的“共享变量”,也就不存在并发问题了。
Jmeter 验证通过
package com.example.demo.config;
import org.springframework.stereotype.Component;
/**
* @version 1.0
* @Description
* @Date 2021/8/2 16:08
* @Author wangyun
*/
@Component
public class ThreadLoaclTest extends ThreadLocal {
@Override
protected Object initialValue() {
return super.initialValue();
}
public ThreadLoaclTest() {
super();
}
@Override
public Object get() {
return super.get();
}
@Override
public void set(Object value) {
super.set(value);
}
@Override
public void remove() {
super.remove();
}
}
五、CAS原子类
CAS的意思是:Compre and Swap ,为 “比较并置换”。CAS机制当中使用了3个基本操作数:内存地址V, 旧的预期值A,要修改的新值B, 只有当内存地址V所对应的值和旧的预期值A相等的时候,才会将内存地址V对应的值更新为新的值B。
在Java中的实现则通常是指以英文Atommic为前缀的一系列类,他们都采用了CAS的思想。
Atomic系列的使用是一种无锁化的CAS操作,是预计乐观锁的,它的并发性能比较高,可以多个线程同时执行,并且保证不会出现安全问题。
让我们来看看AtomicInter 的简单使用:
Jmeter 验证测试,10个线程,循环一次。
实际运行情况,如图4
图4
有两个线程同时去调用access()方法,其中图4中间淡绿色区域的操作是原子操作,要不全部执行,要不全部不执行。
假设此时主存的accessCount的值为0,线程1和线程2同时通过CAS对accessCount的值进行累加,线程1和线程2都需要将accessCount从0更新到1,结果线程1很幸运的成功了,将accessCount的值更新为1,而线程2就失败了,线程2失败后,会再次通过CAS操作进行累加,这时线程2 重新读取最新的accessCount的值为1,接着将accessCount的值从1更新为2,最后就得到了准确的计算结果。
六、Synchronized/ReentrantLock加锁
Synchronized和ReentrantLock都是采用悲观锁的策略,因为他们的试下非常类似,只不过一种是通过语言层面来实现(Synchronized),另一种是通过编程方式实现(ReentrantLock),所以咱们把两种方式放在一起分析了。
先来看看一个 i++的列子,通过这两种方式都能保证线程安全。
代码中lockMethod1使用了ReentrantLock的方式对累加操作进行加锁,在lock()方法调用之后,和unlock方法调用之前的代码能够保证执行的时候是原子性的。如果多个线程同时调用lockMethod1的话,也不会存在线程安全问题。
lockMethod2中,直接在方法上加了synchronized关键字,意味着这个方法执行的的时候也是原子性的,也同样不会存在线程安全问题。
在i++ 这个操作上,主要分为3个步骤:
1、读取i的值,
2、将i的值加1
3、将i的值写会主存中
这上面的3个操作都通过加锁可以保证是原子性的,要么3个操作都执行,要么3个操作都不执行,所以可以解决线程安全问题。
加锁原理如图5:
首先两个线程都去争抢同一个锁,假设线程1获取到了锁,而线程2获取不到锁,就会进入等待队列,等到线程1,执行完代码逻辑之后,会去通知线程2:嘿,哥们儿,我用完了,你可以去尝试获取锁了。这时线程2会重新尝试去获取锁,假如线程2获取锁成功,线程2才开始执行代码。
Jmeter 验证通过
七、总结
最后做一下小结,本篇文章介绍了一些常见的解决并发问题的方法,这些方法分为无锁和有锁两大类。其中无锁的方法有采用局部变量,ThreadLocal、不可变对象、CAS原子类等形式的方法,而有锁也可以分为通过synchronized的方式和ReentrantLock的方式。文中分别对这些方法进行了介绍,并且通过画图和代码分析其中的实现原理,希望大家能好好学习,吸收这些知识。