多线程情况下保护共享资源时也可以使用无锁的方式来实现。
无锁时是通过cas+volatile关键字来实现。
cas: compare and set,意思就是在设置值之前先比较,如果原始值没有发生变化就可以设置成功,否则设置失败。
下面列举几个juc并发包中用来进行无锁并发的类来演示下这种思想
一、AtomicInteger
AtomicInteger
来用来以无锁的方式保护整数类型的变量
1.1 compareAndSet方法
public class Test9 {
private static Logger LOG = LoggerFactory.getLogger(Test9.class);
public static void main(String[] args) {
//给定一个初始值
AtomicInteger num = new AtomicInteger(1);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// compareAndSet接收两个参数:(初始值,要更新成的值)
// 如果没有其他线程修改过,就会修改成功,返回true
// 如果其他线程修改过,初始值就会不匹配,修改失败,返回false
boolean flag = num.compareAndSet(1, 2);
LOG.info("修改结果:{}",flag);
LOG.info("修改后的值:{}",num.get());
}
});
t1.start();
}
}
上边的代码只有一个线程时compareAndSet可以修改成功返回true,当有其他线程修改了num的值时compareAndSet方法会修改失败返回false
//给定一个初始值
AtomicInteger num = new AtomicInteger(1);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// compareAndSet接收两个参数:(初始值,要更新成的值)
// 如果没有其他线程修改过,就会修改成功,返回true
// 如果其他线程修改过,初始值就会不匹配,修改失败,返回false
boolean flag = num.compareAndSet(1, 2);
LOG.info("修改结果:{}",flag);
LOG.info("修改后的值:{}",num.get());
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//线程t2先运行修改num的值为5,t1的修改就会失败
num.compareAndSet(1,5);
}
});
t2.start();
Thread.sleep(1000);
// 1s后启动t1
t1.start();
1s后启动t1因为num已经被修改成5,t1的compareAndSet只有在原有值是1时才会修改成功,所以flag=false,num还是t2设置后的5
实现无锁保护共享资源的关键就在这个方法上,对共享变量赋值时如果compareAndSet方法返回false就重新获最新值来重新尝试,举一个例子来演示。
变量num的初始值设成0,两个线程分别进行100次加和减1操作,最终如果能得到0,就保证了线程安全
//给定一个初始值
AtomicInteger num = new AtomicInteger(0);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//对共享变量赋值的时候循环尝试
while (true){
//获取最新的num值
int pre = num.get();
//旧值,新值
boolean result = num.compareAndSet(pre, pre + 1);
if(result){
//设置成功退出循环
break;
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//对共享变量赋值的时候循环尝试
while (true){
//获取最新的num值
int pre = num.get();
//旧值,新值
boolean result = num.compareAndSet(pre, pre - 1);
if(result){
//设置成功退出循环
break;
}
}
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
LOG.info("num:{}",num);
上边这段代码演示了如何用compareAndSet方法保护共享变量,核心思想是赋值的时候放在一个循环里如果赋值失败就再次获取最新值进行尝试。
但这样写在循环里比较繁琐,所以这个类对这种写法进行了封装,提供了其他几个方法,我们不不需要面向compareAndSet这个方法进行编程
1.2 getAndIncrement
这个方法对变量进行自增,利用cas保证自增操作的原子性即自增操作一定是在最新值的基础上自增。
可以用来替换上边的while true实现原子性自增
1.3 getAndDecrement
这个方法对变量进行自减,利用cas保证自减操作的原子性即自减操作一定是在最新值的基础上自减
可以用来替换上边的while true实现原子性自减
1.4 getAndAccumulate
先看下这个方法的源码
public final int getAndAccumulate(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
// 其中 IntBinaryOperator是一个函数式接口,用来封装调用者传递的操作
public interface IntBinaryOperator {
int applyAsInt(int left, int right);
}
所以可以这样使用
AtomicInteger num = new AtomicInteger(10);
num.getAndAccumulate(5,(pre,x)->pre * x);
LOG.info("res:"+num.get());
可以理解成是用来做原子性的乘法累积运算的,第一个参数表示倍率,第二个参数表示这个倍率如何作用到原有值上。
1.5 getAndUpdate
先看源码
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
// IntUnaryOperator 函数式接口封装操作
public interface IntUnaryOperator {
int applyAsInt(int operand);
}
表示在原有值的基础上进行某种操作,操作的内容由调用者指定。使用cas保证了操作的原子性,即一定是在最新值的基础上完成的这个操作。
AtomicInteger num = new AtomicInteger(10);
num.getAndUpdate(pre->pre*10);
LOG.info("res:"+num.get());
//这个就表示在原有值的基础上乘以10
1.6 卖票的案例
总共有10张票,12个线程来抢,如果没有线程安全问题就会有两个线程抢不到票,直接使用int类型的变量表示总数,因为线程上下文切换,有的线程可能会买到负数票,也可能会买到重复的票。
当然可以使用锁来解决这个问题,以下是使用锁来解决的例子
public class Test9 {
private static Logger LOG = LoggerFactory.getLogger(Test9.class);
public static int total=10;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 12; i++) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Test9.class) {
if(total<=0){
LOG.info("票卖完了");
} else{
try {
//用延时增加线程上下文切换的几率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
total--;
LOG.info("卖到了第{}号票",total);
}
}
}
});
list.add(t1);
}
//启动线程
list.forEach(t->t.start());
}
}
不使用锁时可以用cas操作保证原子性
public class Test9 {
private static Logger LOG = LoggerFactory.getLogger(Test9.class);
public static int total = 10;
public static AtomicInteger atomTotal = new AtomicInteger(total);
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 12; i++) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
//获取最新值
int pre =atomTotal.get();
if (pre <= 0) {
LOG.info("票卖完了");
break;
} else {
try {
//用延时增加线程上下文切换的几率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
int next =pre-1;
// cas操作保证原子性
boolean res = atomTotal.compareAndSet(pre, next);
if(res){
LOG.info("卖到了第{}号票", next);
break;
}
}
}
}
});
list.add(t1);
}
//启动线程
list.forEach(t -> t.start());
}
}
二、AtomicReference
用cas操作保证一个引用类型的变量的地址值有没有被修改过。
主要也是使用其中的 compareAndSet
方法
三、AtomicStampedReference
用cas操作保证一个引用类型的变量的地址值有没有被修改过,多了一个版本号参数,所以可以用来解决ABA问题,
即 A-->B-->A,对另外一个线程来看就像没修改过一样,这个原子类因为有版本号,所以可以识别出这种修改,
主要也是使用其中的 compareAndSet
方法
四、AtomicIntegerArray
用cas操作来保证一个数组中的元素有没有被修改过,看下compareAndSet
方法源码
// i :元素索引
// expect:旧址
// update: 要修改成的值
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
如果数组中对应索引的元素在compareAndSet执行时已经不是expect就会返回false
五、AtomicReferenceArray
用来保证一个元素是引用类型的数组中的元素是否被修改过。
Student[] stuArr;类似这种,这种情况数组中存的是对象的地址值,所以看的是这个地址值是否变过,修改具体对象的属性是不能被感知到的。如果要保护对象属性,需要使用原子更新器(AtomicIntegerFieldUpdater
),并且属性还要是volatile修饰的