栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

CAS原理的秘密:硬件支持(九)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

CAS原理的秘密:硬件支持(九)

文章导读:并发编程理论篇
(一)可见性,原子性,有序性的源头
(二)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也可以。

FutureTask

FutureTask 是一个实实在在的工具类,这个工具类有两个构造函数,它们的参数和前面介绍的 submit() 方法类似,所以这里我就不再赘述了。

FutureTask(Callable callable);
FutureTask(Runnable runnable, V result);

那如何使用 FutureTask 呢?其实很简单,FutureTask 实现了 Runnable 和 Future 接口,由于实现了 Runnable 接口,所以可以将 FutureTask 对象作为任务提交给 ThreadPoolExecutor 去执行,也可以直接被 Thread 执行;又因为实现了 Future 接口,所以也能用来获得任务的执行结果。下面的示例代码是将 FutureTask 对象提交给 ThreadPoolExecutor 去执行。

// 创建FutureTask
FutureTask futureTask
  = new FutureTask<>(()-> 1+2);
// 创建线程池
ExecutorService es = 
  Executors.newCachedThreadPool();
// 提交FutureTask 
es.submit(futureTask);
// 获取计算结果
Integer result = futureTask.get();

FutureTask 对象直接被 Thread 执行的示例代码如下所示。相信你已经发现了,利用 FutureTask 对象可以很容易获取子线程的执行结果。

// 创建FutureTask
FutureTask futureTask
  = new FutureTask<>(()-> 1+2);
// 创建并启动线程
Thread T1 = new Thread(futureTask);
T1.start();
// 获取计算结果
Integer result = futureTask.get();
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/855216.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号