多线程开发中确保这三大特性。首先,最简单的方式就是使用 synchronized 关键字或者其它加锁。这种方式最大的好处是–简单!不需要动脑子,在需要的地方加锁就好了。同步方式在并发时包治百病,但治病的手段却是让多线程程序转为串行执行,这相当于自毁武功,浪费资源。如果滥用同步,那么程序就是去了多线程的意义。因此,只有在必要的时候才使用同步。比如对共享资源的访问。而且尽量控制同步代码块的范围,不需要使用同步的代码,尽量不要放入同步代码块。所以java还提供了轻量级的实现,来解决特定的问题,虽然没有普遍性,只是针对某些特定的问题提供的实现。但是这样已经能够解决问题,还能提高代码效率。
原子性的轻量级实现-Atomic
Atomic 相关类在 java.util.concurrent.atomic 包中。针对不同的原生类型及引用类型,有 AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference 等。另外还有数组对应类型 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。由于 Atomic 提供的功能类似,就不一个个过了。我们以 AtomicInteger 为例,看看 Atomic 类型变量所能提供的功能。
我们首先举个例子来验证AtomicInteger 的原子性
首先我们看没有加这个字段修饰的代码以及运行结果
public class T01_AtomicInterger {
Integer count = new Integer(0);
void m() {
for (int i = 0; i < 10000; i++) {
count++; //count++
}
}
public static void main(String[] args) {
T01_AtomicInterger t = new T01_AtomicInterger();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
这段代码时启动了十个线程,每个线程执行 10000次对count的加一操作,最后的结果理论上是100000。但是结果却不是
运算逻辑是对变量 count 的累加。假如 count 为 int 类型,多个线程并发时,可能各自读取到了同样的值,也可能 A 线程读到 2,但由于某种原因更新晚了,count 已经被其它线程更新为了 4,但是线程 A 还是继续执行了 count+1 的操作,count 反而被更新为更小的值 3。现在的多线程程序是不安全的。故会造成上述的结果,其实我们有最简单的解决办法加锁,就是让加一操作由异步变成同步就行,我们看下边的代码:
synchronized void m() {
for (int i = 0; i < 10000; i++) {
count++; //count++
}
}
最后的运算结果如下:
确实得到 了正确的答案,但是我上次说过了,加锁是需要开销的。所以java提供了更加轻量级的实现--AtomicInteger
我么需要将count用 AtomicInteger修饰,加一的操作不需要用synchronized 修饰了,直接使用Atomic的incrementAndGet方法,也就是加一操作,这样就实现了原子性操作。
AtomicInteger count = new AtomicInteger(0);
/*synchronized */ void m() {
for (int i = 0; i < 10000; i++) {
count.incrementAndGet(); //count++
}
}
运算结果如下;
也是可以实现加一操作的,而且保证了安全性,并且没有加锁,提高了代码的运行效率。其实count=count+1 这行语句其实隐含了两步操作,第一步取得 count 的值,第二步为 count 加 1 。而在这两步操作中间,count 的值可能已经改变了。而 AtomicInteger 提供的 incrementAndGet () 方法,则把这两步操作作为一个原子性操作来完成,则不会出现线程安全问题。
而Atomic如何实现了原子性呢?其实使用了CAS算法。
CAS 算法
CAS 是 Compare and swap 的缩写,翻译过来就是比较替换。其实 CAS 是乐观锁的一种实现。而 Synchronized 则是悲观锁。这里的乐观和悲观指的是当前线程对是否有并发的判断。
悲观锁–认为每一次自己的操作大概率会有其它线程在并发,所以自己在操作前都要对资源进行锁定,这种锁定是排他的。悲观锁的缺点是不但把多线程并行转化为了串行,而且加锁和释放锁都会有额外的开支。
乐观锁–认为每一次操作时大概率不会有其它线程并发,所以操作时并不加锁,而是在对数据操作时比较数据的版本,和自己更新前取得的版本一致才进行更新。乐观锁省掉了加锁、释放锁的资源消耗,而且在并发量并不是很大的时候,很少会发生版本不一致的情况,此时乐观锁效率会更高。
Atomic 变量在做原子性操作时,会从内存中取得要被更新的变量值,并且和你期望的值进行比较,期望的值则是你要更新操作的值。如果两个值相等,那么说明没有其它线程对其更新,本线程可以继续执行。如果不等,说明有线程已经先于此线程进行了更新操作。那么则继续取得该变量的最新值,重复之前的逻辑,直至操作成功。这保证了每个线程对 Atomic 变量操作是线程安全的。
CAS缺点 : ABA 问题
假如本线程更新前取得期望值为 A,和更新操作之间的这段时间内,其它线程可能把 value 改为了 B 又改回了 A。 而本线程更新时发现 value 和期望值一样还是 A,认为其没有变化,则执行了更新操作。但其实此时的 A 已经不是彼时的 A 了。