问题引出
大家可能听过「Automic」原子类,这些类在多线程下可以保证线程安全。比如想要实现自增,多线程下会出现少增加的情况。
代码语言:javascript复制public class VolatileAtomicTest {
public static int num = 0;
public static void increase() {
num ;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i ) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j ) {
increase();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
}
上面代码创建了10个线程,这10个线程要依次循环1000次,每次增加1,最后要求num = 10000。
但实际情况是
多线程情况下会出现第一个线程还在运算,第二个线程就运算完并覆盖了第一个线程的值运算结果,所以会出现与预期不符的结果。原因在
代码语言:javascript复制public static void increase() {
num ;
}
num
我们可以拆解为
int a = num 1; //步骤1
num = a; //步骤2
如果线程一只执行到步骤1,还没执行到步骤2,线程二这时执行了步骤2。那么num的值就是线程2计算的值,而线程一的值就覆盖了。
如果我们保证这两步的原子性(操作一体,不能被其他线程插入)就可以得到预期结果。我们在这个方法下面加锁即可。
代码语言:javascript复制public synchronized static void increase() {
num ;
}
或者
代码语言:javascript复制static Lock lock = new ReentrantLock();
public static void increase() {
try {
lock.lock();
num ;
} finally {
lock.unlock();
}
}
但是,上面的方法都使用了锁,在多线程下对性能还是有影响的。我们可以使用无锁化的原子类,实现原子自增。
代码语言:javascript复制@Test
public void test() throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i ) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j ) {
num = atomicInteger.incrementAndGet();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
原子类
上图就是Java原子类的全家桶,主要是通过CAS 自旋实现的。这里我们主要说说AtomicInteger
,来看看incrementAndGet()
方法。Java8增加了一些类,优化自选带来的性能问题。
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
代码语言:javascript复制/ setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
发现用了Unsafe
的getAndAddInt
方法。至于Unsafe
,底层也是操作系统的类,可以直接修改操作系统内存,或者调度线程。看着名字就知道不建议程序员使用它。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);//从该对象对应地址取出变量值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 var4));
return var5;
}
var1
:Object
var2
:valueOffset
var5
: 当前该变量在内存中的值var5 var4
: 需要写进去的值
这里就是通过CAS修改值,不断循环,知道修改成功。但在高并发情况下,存在一些问题:
❝高并发量的情况下,由于真正更新成功的线程占少数,容易导致循环次数过多,浪费时间,并且浪费线程资源。 由于需要保证变量真正的共享,「缓存一致性」开销变大。 ❞
getIntVolatile()
方法是系统的本地方法
public native int getIntVolatile(Object var1, long var2);
compareAndSwapInt()
也是本地方法,这里就是常说的CAS了。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
手写AtomicInteger
「首先定义几个变量」
代码语言:javascript复制private volatile int value;
private static long offset;//偏移地址
private static Unsafe unsafe;
我们定义了value
值,用来保存当前的值。offset偏移地址,在类初始化的时候,计算出value变量在对象中的偏移。Unsafe
类,直接操作系统内存。
「初始化变量」
代码语言:javascript复制// 通过Unsafe计算出value变量在对象中的偏移
static {
try {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
unsafe = (Unsafe) theUnsafeField.get(null);
Field field = MyAtomicInteger.class.getDeclaredField("value");
offset = unsafe.objectFieldOffset(field);//获得偏移地址
} catch (Exception e) {
e.printStackTrace();
}
}
我们反射获取Unsafe
的theUnsafe
字段,自定义的value
字段,还有偏移地址offset
。
「自增方法」
代码语言:javascript复制public void increment(int num) {
int tempValue;
do {
tempValue = unsafe.getIntVolatile(this, offset);//拿到值
} while (!unsafe.compareAndSwapInt(this, offset, tempValue, value num));//CAS自旋
}
「测试结果」
代码语言:javascript复制public class MyAtomTest {
public static void main(String[] args) {
Thread[] threads = new Thread[10];
MyAtomicInteger atomicInteger = new MyAtomicInteger();
for (int i = 0; i < 10; i ) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j ) {
atomicInteger.increment(1); //自增1
}
});
threads[i].start();
}
for (int i = 0; i < threads.length; i ) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("x=" atomicInteger.get());
}
}
「Atomic」原子类解决了高并发下线程安全问题,但是高并发下也带来了性能问题,如果你的项目需要使用原子类,并且性能要求高,可以使用「Java8」中的原子类。