目录
问题引出
我们模拟一个存款操作,代码如下
private static int money = 0;
@Test
public void plus() throws InterruptedException {
int[] data = new int[] {100,200,300};
for (int i = 0; i < data.length; i++) {
final int temp = i;
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
money += data[temp];
}).start();
}
TimeUnit.SECONDS.sleep(2);
System.out.println(money);
}
输出结果:
此时,这个操作就会出现不同步的设计问题,按照最传统的思路来解决问题,就必须使用同步方法。
private static int money = 0;
@Test
public void plus() throws InterruptedException {
int[] data = new int[] {100,200,300};
for (int i = 0; i < data.length; i++) {
final int temp = i;
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
save(data[temp]);
}).start();
}
TimeUnit.SECONDS.sleep(2);
System.out.println("【存入的金额为】"+money);
}
public synchronized void save(int m){
money += m;
}
结果
思考,难道现在做一个普通的数据计算都要考虑到这种同步的处理机制吗?实在太繁琐了。为了解决这样的解决问题,JDK提供了J.U.C的原子操作类,那么下面使用J.U.C的原子类进行同步
原子操作类
原子操作类并没有使用到传统的同步机制,而是通过一种CAS 的机制来完成的,整个原子类采用了类似的实现机制。
在java.util.concurrent.atomic 包中提供了多种原子性的操作类支持,这些操作可以分为四类:
- 基本类型:AtomicInteger、AtomicLong、AtomicBoolean;
- 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;
- 引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference;
- 对象的属性修改类型:AtomicIntegerFieldUpdate、AtomicLongFieldUpdate、AtomicReferenceFieldUpdate。
原则:所有的原子类都具有同步的支持,但是考虑到性能问题,没有使用到Synchronized 关键字来实现,其依靠底层实现的。
基础类型原子操作类
基础类型的原子类一共提供了三个:AtomicInteger、AtomicLong、AtomicBoolean,首先我们来观察这些类的集成结构
public class AtomicInteger extends Number implements java.io.Serializable
public class AtomicLong extends Number implements java.io.Serializable
public class AtomicBoolean implements java.io.Serializable
其中,AtomicInteger 和 AtomicLong 都属于Number 的子类,而AtomicBoolean 是Object 的子类;
AtomicInteger
范例
// 定义原子操作类
private static AtomicInteger atomicMoney = new AtomicInteger(0);
/** 原子操作类 **/
@Test
public void plusAtomic() throws InterruptedException {
int[] data = new int[] {100,200,300};
for (int i = 0; i < data.length; i++) {
final int temp = i;
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicMoney.addAndGet(data[temp]);
}).start();
}
TimeUnit.SECONDS.sleep(2);
System.out.println("【atomic存入的金额为】"+atomicMoney.get());
}
以上的代码,利用原子类解决了 synchronized 同步的设计问题了,但是这个原子类并没有使用传统的同步机制,我们观察它的源代码;
源代码
package java.util.concurrent.atomic;
import java.lang.invoke.VarHandle;
import java.util.function.IntBinaryOperator;
import java.util.function.IntUnaryOperator;
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public final int addAndGet(int delta) {
return U.getAndAddInt(this, VALUE, delta) + delta;
}
}
AtomicLang
范例
我们设置一个初始金额,然后每次增加100
@Test
public void testAtomicLang() throws InterruptedException {
AtomicLong atomicLong = new AtomicLong(0);
for (int i = 0; i < 3; i++) {
new Thread(()->{
System.out.printf(
"【%s】数据的加法计算:%d %n"
,Thread.currentThread().getName(),atomicLong.addAndGet(100));
}).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println("【计算完成】最终计算结果:"+atomicLong.get());
}
输出结果
通过以上的分析对于原子操作类的基本使用已经没有太大的问题了,那么后面需要面对的就是如果分析具体操作的实现,下面来逐步的对当前类的实现源代码进行解析。
源代码
1、观察 长整型 的基本定义
package java.util.concurrent.atomic;
import java.lang.invoke.VarHandle;
import java.util.function.LongBinaryOperator;
import java.util.function.LongUnaryOperator;
public class AtomicLong extends Number implements java.io.Serializable {
private static final long serialVersionUID = 1927816293512124184L;
/* 我们会看到这里和atomicInteger 有所不同,
因为Integer 所占的位数为32 为Long所占用的长度是64,
所以在32位的系统中,需要修改两个32位的内容 */
static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
private static native boolean VMSupportsCS8();
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicLong.class, "value");
private volatile long value;
public AtomicLong(long initialValue) {
value = initialValue;
}
Long是属于64位的长度,如果运行在了32位的系统之中,那么就需要有2位去描述long 数据类型,而在进行数据修改的时候必须考虑 2 位 的数据同时修改完成,才可以称为正确的修改。
2、观察增加方法的实现
public final long addAndGet(long delta) {
return U.getAndAddLong(this, VALUE, delta) + delta;
}
3、数据的增加操作依靠的是我们的Unsafe类提供的处理方法
@HotSpotIntrinsicCandidate
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!weakCompareAndSetLong(o, offset, v, v + delta));
return v;
}
/** Volatile version of {@link #getLong(Object, long)} */
@HotSpotIntrinsicCandidate
public native long getLongVolatile(Object o, long offset);
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetLong(Object o, long offset,
long expected,
long x) {
return compareAndSetLong(o, offset, expected, x);
}
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetLong(Object o, long offset,
long expected,
long x);
此时的操作方法是由硬件的CPU的指令处理完成的,它不再是通过Java的运行机制来完成的,这样的优势就是在于速度快,同时又避免了数据的内存之中的互相拷贝所带来的额外开销。
在AtomicLong 类中还提供了一个 “compareAndSet()” 方法,该方法的主要作用是进行数据内容的修改,但是在修改之前,需要首先判断当前所保存的数据是否和内容相同,如果相同,则允许修改,如果不同则不允许修改
4、观察CAS 方法实现修改操作
@Test
public void testCompareAndSet() {
AtomicLong atomicLong = new AtomicLong(100L);
System.err.println(
"【× 原子数据修改】数据修改的结果:" + atomicLong.compareAndSet(200L, 300L));
System.err.println(
"【原子数据获取】新的数据内容:" + atomicLong.get());
System.out.println(
"【√ 原子数据修改】数据修改的结果:" + atomicLong.compareAndSet(100L, 300L));
System.out.println(
"【原子数据获取】新的数据内容:" + atomicLong.get());
}
运行结果
只有在CAS 操作比较成功之后才会进行内容的修改,而如果此时的比较失败是不会修改内容的,这是一种乐观锁的机制。
高性能的CAS处理机制
compareAndSet() 数据修改操作方法在J.U.C 中被称之为CAS 机制,CAS (Compare-And-Swap) 是一条CPU 并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,反之则不修改,这个过程就是原子性操作。
在多线程进行数据修改时,为了保证数据修改的正确性,常规的做法就是使用synchronized 同步锁,但是这种锁属于“悲观锁”,每个线程在操作之前锁定当前的内存区域,而后才可以进行处理,这样一来在高并发的环境下就是严重的影响到性能。
而CAS采用的是一种悲观锁机制,其最大的操作特点是不进行强制性的同步处理,而为了保证数据修改的正确性,添加了一些比较的数据(例如:compareAndSet() 在修改之前需要进行数据的比较),采用的是一种冲突重试的处理机制,这样可以有效的避免线程阻塞问题的出现。在并发竞争不是很激烈的情况下,可以获得较好的处理性能,在JDK1.9 后为了进一步CAS 的操作性能,又追加了硬件处理指令集的支持,可以充分的发挥服务器硬件配置的优势,得到更好的性能处理。