文章导读:并发编程理论篇
(一)可见性,原子性,有序性的源头
(二)Java 如何解决可见性、有序性和原子性问题
(三)为什么存在等待 - 通知机制
(四)宏观角度看并发
(五)管程:并发编程的万能钥匙
(六)Java 为什么再造管程
(七)Semaphore信号量简单介绍
(八)介绍两个同步工具类
前面我们多次提到一个累加器的例子,示例代码如下。在这个例子中,add10K() 这个方法不是线程安全的,问题就出在变量 count 的可见性和 count+=1 的原子性上。可见性问题可以用 volatile 来解决,而原子性问题我们前面一直都是采用的互斥锁方案。
public class Test {
long count = 0;
void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
}
其实对于简单的原子性问题,还有一种无锁方案。Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。比如AtomicLong,AtomicInteger,AtomicXXX…
原子类的使用还是挺简单的,优化如下:
public class Test {
AtomicLong count =
new AtomicLong(0);
void add10K() {
int idx = 0;
while(idx++ < 10000) {
count.getAndIncrement();
}
}
}
无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。那它是如何做到的呢?
其实原子类性能高的秘密很简单,硬件支持而已。CPU 的 CAS 指令本身是能够保证原子性的。
原子类的原理是,通过自旋,只有当目前的值和期望值相等时,才会更新内存的值。
但是,在CAS方案中,存在ABA问题。这个问题可以通过版本号表示来解决。Java 也提供了相关的类,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。
Java 提供的原子类能够解决一些简单的原子性问题,但你可能会发现,上面我们所有原子类的方法都是针对一个共享变量的,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方案。
另外,CAS还有一个缺点,前面说到他的原理是通过自旋。自旋这个操作就是循环无限,直到CAS成功。所以这个自旋操作非常消耗CPU。可能会造成CPU飙升100%的情况,其他线程甚至抢不到CPU资源。所以原子类适合读多写少的场景。
Future的介绍以及Futuretask线程池相关的就不介绍了,我之前写了一遍关于线程池以及CompletableFuture源码的文章,有兴趣的可以去看下。
很多时候,我们在线程池提交任务需要获取返回结果,又或者,没有返回结果,我需要知道它是否运行完成,那 ThreadPoolExecutor 是否提供了相关功能呢?
Java 通过 ThreadPoolExecutor 提供的 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。下面我们先来介绍这 3 个 submit() 方法,这 3 个方法的方法签名如下。
Future// 提交Runnable任务 // 方法返回的 Future 仅可以用来断言任务已经结束了 Future> submit(Runnable task); // 提交Callable任务 // 通过调用其 get() 方法来获取任务的执行结果Future submit(Callable task); // 提交Runnable任务及结果引用 // 不常用 Future submit(Runnable task, T result);
怎么理解Future呢?直译过来就是“未来‘。比如 Future submit(Callable task) 方法,返回一个Future对象,这对象可以实时了解执行这个task的结果。又或者说,线程执行完了,会把结果赋值到返回的这个Future对象中。
实际上,这个Futrue对象主要有5个方法
// 取消任务 boolean cancel( boolean mayInterruptIfRunning); // 判断任务是否已取消 boolean isCancelled(); // 判断任务是否已结束 boolean isDone(); // 获得任务执行结果 get(); // 获得任务执行结果,支持超时 get(long timeout, TimeUnit unit);
除了通过线程池返回的Future对象可以获取返回结果,FutureTask也可以。
FutureTaskFutureTask 是一个实实在在的工具类,这个工具类有两个构造函数,它们的参数和前面介绍的 submit() 方法类似,所以这里我就不再赘述了。
FutureTask(Callablecallable); FutureTask(Runnable runnable, V result);
那如何使用 FutureTask 呢?其实很简单,FutureTask 实现了 Runnable 和 Future 接口,由于实现了 Runnable 接口,所以可以将 FutureTask 对象作为任务提交给 ThreadPoolExecutor 去执行,也可以直接被 Thread 执行;又因为实现了 Future 接口,所以也能用来获得任务的执行结果。下面的示例代码是将 FutureTask 对象提交给 ThreadPoolExecutor 去执行。
// 创建FutureTask FutureTaskfutureTask = new FutureTask<>(()-> 1+2); // 创建线程池 ExecutorService es = Executors.newCachedThreadPool(); // 提交FutureTask es.submit(futureTask); // 获取计算结果 Integer result = futureTask.get();
FutureTask 对象直接被 Thread 执行的示例代码如下所示。相信你已经发现了,利用 FutureTask 对象可以很容易获取子线程的执行结果。
// 创建FutureTask FutureTaskfutureTask = new FutureTask<>(()-> 1+2); // 创建并启动线程 Thread T1 = new Thread(futureTask); T1.start(); // 获取计算结果 Integer result = futureTask.get();



