前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法以及通过jvm内存模型的方式去介绍了一些并发中常见的问题(想看往期文章的小伙伴可以直接拉到文章最下方飞速前往)。本文重点介绍一个概念“无锁”
本期精彩
什么是无锁无锁类的原理
AtomicInteger
Unsafe
AtomicReference
AtomicStampedReference
什么是无锁
在高并发编程中最重要的就是获取临界区资源,保证其中操作的原子性。一般来说使用synchronized关键字进行加锁,但是这种操作方式其实是将synchronized中的代码块由并行转为串行,虽然说这是一个解决并发问题的方法,但是这样的代码效率会显得比较低下。最比较高效的方法就是无锁,一般加锁的方法在多线程访问时,如果临界区资源被占用,系统就会将其他线程进行阻塞,挂起,但是无锁不会,它只会一次一次的重试,直到执行成功为止。在jdk中为我们提供了一系列的无锁类来供我们使用。
无锁类的原理
- CAS(Compare And Swap)比较并交换
CAS算法:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。而失败得线程不会被挂起,只是被通知失败,而线程则会再次尝试,也可以设置失败则不继续尝试访问。 - CAS操作得CPU指令(cmpxchg)
有些人有疑惑,在CAS操作中,步骤如此之多,会不会是非原子操作,如果是非原子操作会不会引起线程不安全的情况。其实CAS操作属于cpu指令cmpxchg完成的,通过指令操作保证为原子操作。
AtomicInteger
AtomicInteger为无锁整数,它其中的方法都是无锁的,它内部主要得接口有以下几个:
方法名 | 返回值 | 参数 | 描述 |
get() | int | 无 | 获取当前值 |
set() | 无 | newValue | 设置当前值 |
getAndSet() | int | newValue | 设置新值,返回旧值 |
compareAndSet() | boolean | int expect, int u | 如果内存中的值为expect,则设置新值为u,并且返回true |
getAndIncrement() | int | 无 | 当前值+1,返回旧值 |
getAndDecrement() | int | 无 | 当前值-1,返回旧值 |
getAndAdd() | int | delta | 当前值增加delta,返回旧值 |
incrementAndGet() | int | 无 | 当前值+1,返回新值 |
decrementAndGet() | int | 无 | 当前值-1,返回新值 |
addAndGet() | int | int delta | 当前值增加delta,返回新值 |
我们来看其中两个比较典型的方法的实现:
- compareAndSet(int expect, int update):这个方法为如果内存中的值为expect,则设置新值为update,并且返回true,反之则设置失败,返回false
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
上述方法中,出现了几个参数valueOffset表示一个偏移量,expect表示一个预期值,update表示一个新的值,而调用得compareAndSwapInt方法则表示,在这个类的valueOffset的偏移量上得值是否与expect的值一致,如果一致,则将值修改为update的值,否则则设置失败。
- getAndIncrement()当前值+1,返回旧值
/**
* Atomically increments by one the current value
*
* @return the previous value
*/
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) {
return current;
}
}
}
getAndIncrement()方法通过死循环的方式确保可以一致进行修改操作,但是一旦修改成功则跳出,否则一直修改。
我们来看一个具体的例子:
/**
* @escription:无锁累加
* @author: Herrt灬凌夜
* @date: 2019年3月2日 下午10:21:53
*/
public class Tets1 {
public AtomicInteger num = new AtomicInteger();
public void accumulation () {
for (int i = 0; i < 10000; i++) {
num.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
Thread [] ts = new Thread[10];
final Tets1 test = new Tets1();
for (int i = 0; i < ts.length; i++) {
ts[i] = new Thread(new Runnable() {
public void run() {
test.accumulation();
}
});
ts[i].start();
}
for (Thread thread : ts) {
thread.join();
}
System.out.println(test.num);
}
}
在上述例子中我并没有对accumulation()方法进行加锁,但是最后得到的结果依旧是100000。所以可以说明这个操作是线程安全的。
Unsafe
Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。但是它是非公开的API,所以在不同得JDK版本中,差异比较大,但是它在JDK开发中应用非常多。
Unsafe类通过偏移量这个概念使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
它内部主要的接口有以下几个:
方法名 | 返回值 | 参数 | 描述 |
getInt() | int | Object o, long offset | 获得给定对象偏移量上的int值 |
putInt() | void | Object o, long offset, int x | 设置给定对象偏移量上的int值 |
objectFieldOffset() | long | Field f | 获得字段在对象中的偏移量 |
putIntVolatile() | void | Object o, long offset, int x | 设置给定对象的int值,使用volatile语义 |
getIntVolatile() | int | Object o, long offset | 获得给定对象的int值,使用volatile语义 |
putOrderedInt | void | Object o, long offset, int x | 和putIntVolatile一样,但是它要求被操作得字段是volatile修饰的 |
上述的几个方法都是被native关键字所修饰,因为Unsafe的实现是由C语言实现的。Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface (Java本地接口)。
AtomicReference
AtomicReference引用做了修改,是一个模版类,抽象了数据类型,如果说AtomicInteger修改的是一个整数,那么AtomicReference修改的就是一个对象。它其中的方法与AtomicInteger的方法大致一致,只是在类上加了一个范型。
我们看下面实例:
/**
* @escription:AtomicReference实例
* @author: Herrt灬凌夜
* @date: 2019年3月3日 下午3:42:38
*/
public class AtomicReferenceTest {
public AtomicReference atomicStr = new AtomicReference("修改前");
public void accumulation () {
if(atomicStr.compareAndSet("修改前", "修改后")) {
System.out.println("Thread:" + Thread.currentThread().getId() + "修改成功!");
} else {
System.out.println("Thread:" + Thread.currentThread().getId() + "修改失败!");
}
}
public static void main(String[] args) {
final AtomicReferenceTest reference = new AtomicReferenceTest();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
reference.accumulation();
}
}).start();
}
}
}
执行上面代码可以得出,只有一个线程修改成功,其他线程均修改失败,可以看出AtomicReference为线程安全的。
AtomicStampedReference
AtomicStampedReference也是用于修改一个对象的,但是这个类中加入了一个邮戳的标记,而这是为了解决ABA问题的,何为ABA问题呢,就是说一个线程将值修改为B,但是又被其他线程修改为A,这样其他线程又会继续去修改A.
我们将AtomicReference中的实例做修改:
public class AtomicReferenceTest {
public AtomicReference atomicStr = new AtomicReference("修改前");
public void accumulation () {
if(atomicStr.compareAndSet("修改前", "修改后")) {
System.out.println("Thread:" + Thread.currentThread().getId() + "修改成功!");
} else {
System.out.println("Thread:" + Thread.currentThread().getId() + "修改失败!");
atomicStr.compareAndSet("修改后", "修改前");
}
}
public static void main(String[] args) {
final AtomicReferenceTest reference = new AtomicReferenceTest();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
reference.accumulation();
}
}).start();
}
}
}
在我们预期之中,修改成功只能被执行一次,但是由于其他线程的原因,执行成功被执行多次。而AtomicStampedReferenve就是来解决这类问题的。
我们来看一个例子,我们模拟用户消费,当用户首次余额不足20元时,系统赠送20元。
/**
* @escription:AtomicStampedReference
* @author: Herrt灬凌夜
* @date: 2019年3月3日 下午6:57:36
*/
public class AtomicStampedReferenceTest {
AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);
/**
* 充值
* @Title: recharge
* @Description: 当余额第一次不足20元时,系统充值20元
* @param: @param timestamp
*/
public void recharge(int timestamp) {
while (true) {
while (true) {
Integer m = money.getReference();
if (m < 20) {
if (money.compareAndSet(m, m + 20, timestamp, timestamp + 1)) {
System.out.println("余额小于20,充值成功,当前余额为:" + money.getReference());
break;
} else {
break;
}
}
}
}
}
/**
* 消费
* @Title: consumption
* @return: void
*/
public void consumption() {
for (int i = 0; i < 100; i++) {
while (true) {
int timestamp = money.getStamp();
Integer m = money.getReference();
if (m > 10) {
if (money.compareAndSet(m, m - 10, timestamp, timestamp + 1)) {
System.out.println("消费10元,余额:" + money.getReference());
break;
}
} else {
System.out.println("余额不足!");
break;
}
break;
}
}
}
public static void main(String[] args) {
final AtomicStampedReferenceTest test = new AtomicStampedReferenceTest();
final int timestamp = test.money.getStamp();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
test.recharge(timestamp);
}
}).start();
}
new Thread(new Runnable() {
public void run() {
test.consumption();
}
}).start();
}
}
执行结果发现,充值只发生1次,不会因为消费之后余额小于20元再次充值。
我们去查看AtomicStampedReference类,发现其中存在一个内部类Pair:
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
}
这个类中的Pair类代替了AtomicReference中的value,其中reference相当于value,而stamp则为一个标识。我们查看compareAndSet的源码:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
我们发现,这里不仅仅去比较了reference的值,也去比较了stamp 的值,只有他们得值都相等,才会去执行cas操作。