当程序更新一个变量时,如果多线程同时更新这个变量,可能得到期望之外的值,比如变量i=0,A线程更新i+1,B线程也更新i+1,经过两个线程操作之后可能i不等于2,而是等于1。因为A和B线程在更新变量i的时候拿到的i都是0,这就是线程不安全的更新操作:
比如这段代码的运行结果已经很难等于50000了(数字再调大一些就更不可能了):
public class Main {
int i = 0;
void inc() {
for (int j = 0; j < 25000; j++) i++;
}
public static void main(String[] args) {
Main main = new Main();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 2; i++) {
threads.add(new Thread(main::inc));
}
threads.forEach(Thread::start);
threads.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(main.i);
}
}
通常我们可以使用synchronized来解决这个问题:
synchronized会锁住整个对象,其余线程想要拿到对象锁的话就只能阻塞:
final Object lock = new Object();
void inc() {
synchronized (lock) {
for (int j = 0; j < 25000; j++) i++;
}
}
void inc() {
synchronized (this) {
for (int j = 0; j < 25000; j++) i++;
}
}
synchronized void inc() {
for (int j = 0; j < 25000; j++) i++;
}
但是synchronized的缺点就是它的悲观锁机制导致其余线程想要读也只能阻塞(别谈什么时间问题,下面会有演示)
而Java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类AtomicXXX
提供了一种用法简单、性能高效、线程安全地更新一个变量的方式:
AtomicInteger i = new AtomicInteger(0);
void inc() {
for (int j = 0; j < 25000; j++) i.incrementAndGet();
}
原子类对多线程自增操作的原子性是通过CAS保证的:
原子类保证原子性是通过CAS实现的
以AtomicInteger
为例,它的核心就是compareAndSet()
方法和getAndAddInt()
方法;其中getAndAddInt()
方法的核心是Unsafe
类的CompareAndSwapInt()
本地方法:
public final boolean compareAndSet(int expect, int update) {
// 期望值 ↑ , ↑ 更新值
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
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;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
如果CAS失败了,则继续循环,执行do块中的内容;否则说明CAS成功,返回交换后的值。
使用原子的方式更新基本类型,java.util.concurrent.atomic
包提供了以下3个类:
Unsafe
类提供的CAS本地方法只有三个:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
因此对int类型的数据更新则是用compareAndSwapInt()
方法,对long类型的数据更新用compareAndSwapLong()
方法,对bool类型的数据进行更新则是先将bool类型的数据转化为int类型的数据,然后再使用compareAndSwapInt()
实现:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
使用原子的方式更新数组,java.util.concurrent.atomic
包提供了以下3个类:
以AtomicIntegerArray
为例,它的方法与AtomicInteger
大致相同,只是多了个参数为数组下标比如:
addAndGet(int i, int delta)
就是将数组下标为i的元素加上delta并返回:
AtomicIntegerArray integerArray = new AtomicIntegerArray(new int[]{1,2});
System.out.println(integerArray.addAndGet(0, 1)); // 2
System.out.println(integerArray); // [2, 2]
数组通过构造方法传递进去,然后AtomicIntegerArray
会将当前数组复制一份,所以当AtomicIntegerArray
对内部的数组元素进行修改时,不会影响传入的数组。
使用原子的方式更新引用类型,java.util.concurrent.atomic
包提供了以下3个类:
User user0 = new User("小明", 18);
AtomicReference<User> userAtomicReference = new AtomicReference<>(user0);
System.out.println(userAtomicReference.get()); // User{name='小明', age=18}
System.out.println(userAtomicReference.compareAndSet(user0, new User("小红", 20))); // true
System.out.println(userAtomicReference.get()); // User{name='小红', age=20}
User user0 = new User("小明", 18);
AtomicReferenceFieldUpdater<User, String> updater = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
System.out.println(user0); // User{name='小明', age=18}
System.out.println(updater.compareAndSet(user0, "小明", "小红")); // true
System.out.println(user0); // User{name='小红', age=18}
AtomicMarkableReference<User> userMarkable=new AtomicMarkableReference<>(user0,true);
System.out.println(userMarkable.getReference()); // User{name='小明', age=18}
System.out.println(userMarkable.compareAndSet(user0, new User("小红", 20), true, false)); // true
System.out.println(userMarkable.getReference()); // User{name='小红', age=20}
System.out.println(userMarkable.compareAndSet(user0, new User("小白", 30), true, false)); // false
System.out.println(userMarkable.getReference()); // User{name='小红', age=20}
要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用public volatile
修饰符。
使用原子的方式更新引用类型,java.util.concurrent.atomic
包提供了以下3个类:
User user0 = new User("小明", 18);
AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
System.out.println(user0); // User{name='小明', age=18}
System.out.println(updater.compareAndSet(user0, 18, 20)); // true
System.out.println(user0); // User{name='小明', age=20}
AtomicStampedReference
解决方案public class Main {
static AtomicReference<String> string = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": A -> B :" + string.compareAndSet("A", "B"));
});
t1.start();
t1.join();
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": B -> A :" + string.compareAndSet("B", "A"));
});
t2.start();
t2.join();
System.out.println(Thread.currentThread().getName() + ": A -> C :" + string.compareAndSet("A", "C"));
}
}
AtomicStampedReference
解决ABA问题public class Main {
static AtomicStampedReference<String> string = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
int s = string.getStamp();
Thread t1 = new Thread(() -> {
int stamp = string.getStamp();
System.out.println(Thread.currentThread().getName() + ": A -> B :" + string.compareAndSet("A", "B", stamp, stamp + 1) + " -- stamp: " + stamp + " -> " + (stamp + 1));
});
t1.start();
t1.join();
Thread t2 = new Thread(() -> {
int stamp = string.getStamp();
System.out.println(Thread.currentThread().getName() + ": B -> A :" + string.compareAndSet("B", "A", stamp, stamp + 1) + " -- stamp: " + stamp + " -> " + (stamp + 1));
});
t2.start();
t2.join();
System.out.println(Thread.currentThread().getName() + ": A -> C :" + string.compareAndSet("A", "C", s, s + 1) + " -- stamp: " + s + " -> " + (s + 1) + " x ");
}
}
LongAdder
继承自Striped64
。
LongAddr
和Atomic
都能保证long类型数据的原子性,但是LongAddr
的性能要优于AtomicLong
:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
test(
AtomicLong::new,
AtomicLong::getAndIncrement,
"AtomicLong"
);
}
for (int i = 0; i < 10; i++) {
test(
LongAdder::new,
LongAdder::increment,
"LongAdder"
);
}
}
public static <T> void test(Supplier<T> supplier, Consumer<T> consumer, String name) {
T adder = supplier.get();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) consumer.accept(adder);
}));
}
long start = System.nanoTime();
list.forEach(Thread::start);
list.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(name + ":" + adder + ":用时 " + (end - start) / 1000000);
}
}
LongAdder
比AtomicLong
性能高的原因AtomicLong
使用CAS,在并发量很大时,会有大量线程在CAS自旋等待,因此耗时;而LongAdder
采用的累加单元(Cell)的方式:在竞争时,设置多个累加单元,Thread-0累加Cell[0],Thread-1累加Cell[1]…(类似于分段锁的原理)最后将结果汇总。这样他们在累加时操作的是不同的Cell变量,减少了CAS的重试次数(DoubleAdder
也是一样的道理)。
LongAdder
的累加单元数量不超过CPU核数,因此CPU核数越多的电脑性能提升越明显。
synchronized的效率并非永远都要低于原子类,但是他们的性能都要远远低于Adder:
public class Main {
Long count0 = 0L;
AtomicLong count1 = new AtomicLong(0);
LongAdder count2 = new LongAdder();
synchronized void synchronizedInc() {
for (int i = 0; i < 10000; i++) {
count0++;
}
}
void atomicInc() {
for (int i = 0; i < 10000; i++) {
count1.incrementAndGet();
}
}
void adderInc() {
for (int i = 0; i < 10000; i++) {
count2.add(1);
}
}
public static void main(String[] args) {
long start0 = System.currentTimeMillis();
Main t0 = new Main();
List<Thread> threads0 = new ArrayList<>();
for (int i = 0; i < 10; i++) threads0.add(new Thread(t0::synchronizedInc));
threads0.forEach(Thread::start);
threads0.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t0.count0 + "-->synchronized耗时-->" + (System.currentTimeMillis() - start0));
System.out.println("-----------------------------");
long start1 = System.currentTimeMillis();
Main t1 = new Main();
List<Thread> threads1 = new ArrayList<>();
for (int i = 0; i < 10; i++) threads1.add(new Thread(t1::atomicInc));
threads1.forEach(Thread::start);
threads1.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t1.count1 + "-->AtomicLong耗时-->" + (System.currentTimeMillis() - start1));
System.out.println("-----------------------------");
long start2 = System.currentTimeMillis();
Main t2 = new Main();
List<Thread> threads2 = new ArrayList<>();
for (int i = 0; i < 10; i++) threads2.add(new Thread(t2::adderInc));
threads2.forEach(Thread::start);
threads2.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t1.count1 + "-->LongAdder耗时-->" + (System.currentTimeMillis() - start2));
}
}
Adder的效率肯定是最好的,那我们就看看synchronized和原子类:
当自增次数较少时,原子类的效率是优于synchronized的:
当自增次数达到一定次数时,synchronized和原子类的性能就相近了:
超过这个阈值后,synchronized的性能就会优于原子类,且值越大优势越明显:
因为并发量大时,原子类CAS出错概率会增大导致不断自旋等待,这是非常消耗时间的;而synchronized虽然导致线程阻塞等待,但是由于自增操作非常快,耗时很短,所以每个线程都不会阻塞过长的时间,此时synchronized的性能就会优于原子类。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/qq_43613793/article/details/120449394
内容来源于网络,如有侵权,请联系作者删除!