高并发编程之无锁
Posted wuyx
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发编程之无锁相关的知识,希望对你有一定的参考价值。
前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法以及通过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操作。
以上是关于高并发编程之无锁的主要内容,如果未能解决你的问题,请参考以下文章