JAVA多线程并发编程-避坑指南

Posted Jcloud

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA多线程并发编程-避坑指南相关的知识,希望对你有一定的参考价值。

本篇旨在基于编码规范、工作中积累的研发经验等,整理在多线程开发的过程中需要注意的部分,比如不考虑线程池参数、线程安全、死锁等问题,将会存在潜在极大的风险。并且对其进行根因分析,避免每天踩一坑,坑坑不一样。

作者:京东零售 肖朋伟

一、前言

开发过程中,多线程的应用场景可谓十分广泛,可以充分利用服务器资源,提高程序处理速度。我们通常也会使用池化技术,去避免频繁创建和销毁线程。

本篇旨在基于编码规范、工作中积累的研发经验等,整理在多线程开发的过程中需要注意的部分,比如不考虑线程池参数、线程安全、死锁等问题,将会存在潜在极大的风险。并且对其进行根因分析,避免每天踩一坑,坑坑不一样。

二、多线程并发场景有哪些坑?

1、“不正确的创建”线程池

常规来说,线程资源必须通过线程池提供,不允许在应用中自行显式创建线程,京东 JAVA 代码规范也明确表示 “线程资源必须通过线程池提供,不允许在应用中自行显式创建线程”,但是创建线程池的方式也有很多种,不能滥用。

常见创建线程池方式如:通过 JDK 提供的 ThreadPoolExecutor、ScheduledThreadPoolExecutor 以及 JDK 7 开始引入的 ForkJoinPool 创建,还有更方便的 Executors 类以及 spring 提供的 ThreadPoolTaskExecutor 等。其关系如下图:

Executors 的 newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor、newCachedThreadPool 方法在底层都是用的 ThreadPoolExecutor 实现的。虽然更加方便,但也增加了风险,如果不清楚其相关实现,在特定场景可能导致很严重的问题,所以开发规范中,会严格禁用此类用法。它们的弊端如下:

(1)FixedThreadPool 和 SingleThreadPool 允许的请求任务队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

(2)CachedThreadPool 和 ScheduledThreadPool 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

2、线程池“参数设置不合理”

上面提到了 任务队列 和 最大线程数的重要性,下面通过 ThreadPoolExecutor 介绍线程池其他核心参数,都应该根据场景合理配置,详细如下:

注意在设置任务队列时,需要考虑有界和无界队列,使用有界队列时,注意线程池满了后,被拒绝的任务如何处理。使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。

对于拒绝策略,首先要明确的一点是“拒绝策略的执行线程是提交任务的线程,而不是子线程”。JDK 的 ThreadPoolExecutor 提供了 4 种拒绝策略:

对于自定义拒绝策略,不同场景应选择相应的拒绝策略,抛开应用场景讲技术会显得苍白,大家可以参考常见技术框架的处理方式:

相关拓展:

相信大多数京东开发者都遇到过,JSF 依赖服务触发拒绝策略的现象,即抛出线程拒绝异常。这是当 Provider的 业务线程池满了,无可用线程池的时候,会返回一个异常给 Consumer,告知 Consumer 该 Provider 线程池已耗尽。如图:

当然这种异常场景,根本原因并非线程池配置不合理,应该关注服务提供方性能瓶颈,关于线程池的配置,其实没用一个统一或者可推荐的配置可以套用。对于 JSF 业务线程池,默认使用的是伸缩无队列线程池,其也提供了配置方式。

3、局部线程池“使用后不销毁回收”

线程会消耗宝贵的系统资源,比如内存等,所以是很不推荐使用局部线程池(未预先创建的线程池,用完就可以销毁,下次用时还会创建)的;但是如果某些特殊场景确实使用了局部线程池,那么应该在用完后,主动销毁。主动销毁线程池主要有两种方式:

为了更深入的理解两个问题:

(1)到底是否所有局部创建的线程池都需要主动销毁?

(2)为什么 Dubbo 中的线程拒绝策略 AbortPolicyWithReport 使用了 Executors.newSingleThreadExecutor(),并且没有主动销毁动作?

我们需要从 GC 角度进行分析。要知道对象什么时候死亡,我们需要先知道 JVM 的 GC 是如何判断对象是可以回收的。JAVA 是通过可达性算法来来判断对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

对于线程池而言,在 ThreadPoolExecutor 类中具有非静态内部类 Worker,用于表示当前线程池中的线程,因为非静态内部类对象具有外部包装类对象的引用,所以当存在线程 Worker 对象时,线程池不会被 GC 回收。也就是说,线程池没有引用,且线程池内没有存活线程时,才是可以被 GC 回收的。应注意的是线程池的核心线程默认是一直存活的,除非核心线程数为 0 或者设置了 allowCoreThreadTimeOut 允许核心消除空闲时销毁。

我们对 Executors 创建的三种线程池进行比较:

三种类型的线程池与 GC 关系:

(1)CachedThreadPool:没有核心线程,且线程具有超时时间,可见在其引用消失后,等待任务运行结束且所有线程空闲回收后,GC开始回收此线程池对象;

(2)FixedThreadPool:核心线程数及最大线程数均为 nThreads,并且在默认 allowCoreThreadTimeOut 为 false 的情况下,其引用消失后,核心线程即使空闲也不会被回收,故 GC 不会回收该线程池;

(3)SingleThreadExecutor:在创建时实际返回的是 FinalizableDelegatedExecutorService 类的对象,该类重新了 finalize() 函数执行线程池的销毁,该对象持有 ThreadPoolExecutor 对象的引用,但 ThreadPoolExecutor 对象并不引用 FinalizableDelegatedExecutorService 对象,这使得在 FinalizableDelegatedExecutorService 对象的外部引用消失后,GC 将会对其进行回收,触发 finalize 函数,而该函数仅仅简单的调用 shutdown 函数关闭线程,在所有当前的任务执行完成后,回收线程池中线程,则 GC 可回收线程池对象

所以结论是:CachedThreadPool 及 SingleThreadExecutor 的对象在不显式销毁时,且其对象引用消失的情况下,可以被 GC 回收;FixedThreadPool 对象在不显式销毁,且其对象引用消失的情况下不会被 GC 回收,会出现内存泄露。因此无论使用什么线程池,使用完毕后均调用 shutdown 是一个较为安全的编程习惯。

4、线程池处理“刚启动时效率低”

默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。对于 ThreadPoolExecutor 线程池可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。

5、“不合理的使用共享线程池”

提交异步任务,不指定线程池,存在最主要问题是非核心业务占用线程资源,可能会导致核心业务收影响。因为公共线程池的最大线程数、队列大小、拒绝策略都比较保守,可能引发各种问题,常见场景如下:

(1)CompleteFuture 提交异步任务,不指定线程池。

CompleteFuture 的 supplyAsync 等以 *Async 为结尾的方法,会使用多线程异步执行。可以注意到的是,它也允许我们不携带多线程提交任务的执行线程池参数,这个时候是默认使用的 ForkJoinPool.commonPool()。

ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。ForkJoinPool 默认线程数取决于 parallelism 参数为:CPU 处理器核数-1,也允许通修改 Java 系统属性 "java.util.concurrent.ForkJoinPool.common.parallelism" 进行自定义配置。

(2)JDK 8 引入的集合框架的并行流 Parallel Stream。

Parallel Stream 也是使用的 ForkJoinPool.commonPool(),但有一点区别是:Parallel Stream 的主线程 (提交任务的线程)是会去参与处理的;比如 8 核心的机器执行 Parallel Stream 是有 8 个线程,而 CompleteFuture 提交的任务只有 7 个线程处理。不建议使用是一致的。

(3)@Async 提交异步任务,不指定线程池。

SpringBoot 2.1.9 之前版本使用 @Async 不指定 Executor 会使用 SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发 OutOfMemoryErro r错误。SpringBoot 2.1.0 之后版本引入了 TaskExecutionAutoConfiguration,其使用 ThreadPoolTaskExecutor 作为 默认 Executor,通过 TaskExecutionProperties.Pool 可以看到其配置默认核心线程数:8,最大线程数:Integet.MAX_VALUE,队列容量是:Integet.MAX_VALUE,空闲线程保留时间:60s,线程池拒绝策略:AbortPolicy。

虽然可以通实现 AsyncConfigurer 接口等方式,自行配置线程池参数,但仍不建议使用公共线程池。

6、主线程“等待时间不合理”

(1)应尽量避免使用 CompletableFuture.join(),Future.get() 这类不带有超时时间的阻塞主线程操作。

(2)for 循环使用 future.get(long timeout, TimeUnit unit)。此方法允许我们去设置超时时间,但是如果主线程串行获取的话,下一个 future.get 方法的超时时间,是从第一个 get() 结束后开始计算的,所以会导致超时时间不合理。

7、提交任务“不考虑子线程超时时间”

(1)主线程 Future.get 虽然超时,但是子线程依然在执行?

比如当通过 ExecutorService 提交一个 Callable 任务的时候,会返回一个 Future 对象,Future 的 get(long timeout, TimeUnit unit) 方法时,如果出现超时,则会抛出 java.util.concurrent.TimeoutException;但是,此时 Future 实例所在的线程并没有中断执行,只是主线程不等待了,也就是当前线程的 status 依然是 NEW 值为 0 的状态,所以当大量超时,可能就会将线程池打满。

提到中断子线程,会想到 future.cancel(true) 。那么我们真的可以中断子线程吗?首先 Java 无法直接其他线程的,如果非要实现此功能,也只能通过 interrupt 设置标志位,子线程执行到中间环节去查看标志位,识别到中断后做后续处理。理解一个关键点,interrupt() 方法仅仅是改变一个标志位的值而已,和线程的状态并没有必然的联系。

(2)子线程的任务都应该有一个合理的超时时间。

比如子线程调用 JSF/ HTTP 接口等,一定要检查超时时间配置是否合理。

8、并发执行“线程不安全”操作

多线程操作同一对象应考虑线程安全性。常见场景比如 HashMap 应该换成 ConcurrentHashMap;StringBuilder 应该换成 StringBuffer 等。

9、“不考虑线程变量”的传递

提交到线程池执行的异步任务,切换了线程,子线程在执行时,获取不到主线程变量中存储的信息,常见场景如下:

(1)类似 BU 等,为了减少参数透传,可能存在了 ThreadLocal 里面;

(2)客户的登录状态,LoginContext 等信息,一般是线程变量;

如果解决此问题,可以参考 Transmittable-Thread-Local 中间件提供的解决方案等。

10、并发会“增大出现死锁的可能性”

多线程不只是程序中提交到线程池执行,比如打到同一容器的 http 请求本身就是多线程,任何多线程操作都有死锁风险。使用业务线程池的并发操作需要更加注意,因为更容易暴露出来“死锁”这个问题。

比如 Mysql 事务隔离级别为 RR 时,间隙锁可能导致死锁问题。间隙锁是 Innodb 在可重复读提交下为了解决幻读问题时引入的锁机制,在执行 update、delete、select ... for update 等语句时,存在以下加间隙锁情况:

(1)有索引,当更新的数据存在时,只会锁定当前记录;更新的不存在时,间隙锁会向左找第一个比当前索引值小的值,向右找第一个比当前索引值大的值(没有比当前索引值大的数据时,是 supremum pseudo-record,可以理解为锁到无穷大)。

(2)无索引,全表扫描,如果更新的数据不存在,则会根据主键索引对所有间隙加锁。

当并发执行数据库事务(事务内先更新,后新增操作),当更新的数据不存在时,会加间隙锁,然后执行新增数据需要其他事务释放在此区间的间隙锁,则可能导致死锁产生;如果是全表扫描,问题可能更严重。

11、“不考虑请求过载”

最后,我们设置了合理的参数,也注意优化了各种场景问题,终于可以大胆使用多线程了。也一定要考虑对下游带来的影响,比如数据库请求并发量增长,占用 JDBC 数据库连接、给依赖 RPC 服务带来性能压力等。

参考文章链接:

http://www.kailing.pub/article/index/arcid/255.html

https://blog.csdn.net/sinat_15946141/article/details/107951917

https://segmentfault.com/a/1190000016461183?utm_source=tag-newest

https://blog.csdn.net/v123411739/article/details/106609583/

https://blog.csdn.net/whzhaochao/article/details/126032116

https://www.kuangstudy.com/bbs/1407940516238090242

Java并发指南开篇:Java并发编程学习大纲

Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容。

这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类。当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中涉及到的技术原理十分丰富。为了更好地把并发知识形成一个体系,也鉴于本人没有能力写出这类文章,于是参考几位并发编程专家的博客和书籍,做一个简单的整理。

一:并发基础和多线程

首先需要学习的就是并发的基础知识,什么是并发,为什么要并发,多线程的概念,线程安全的概念等。

然后学会使用Java中的Thread或是其他线程实现方法,了解线程的状态转换,线程的方法,线程的通信方式等。

 

二:JMM内存模型

任何语言最终都是运行在处理器上,JVM虚拟机为了给开发者一个一致的编程内存模型,需要制定一套规则,这套规则可以在不同架构的机器上有不同实现,并且向上为程序员提供统一的JMM内存模型。

所以了解JMM内存模型也是了解Java并发原理的一个重点,其中了解指令重排,内存屏障,以及可见性原理尤为重要。

JMM只保证happens-before和as-if-serial规则,所以在多线程并发时,可能出现原子性,可见性以及有序性这三大问题。

下面的内容则会讲述Java是如何解决这三大问题的。

 

三:synchronized,volatile,final等关键字

对于并发的三大问题,volatile可以保证原子性和可见性,synchronized三种特性都可以保证(允许指令重排)。

synchronized是基于操作系统的mutex lock指令实现的,volatile和final则是根据JMM实现其内存语义。

此处还要了解CAS操作,它不仅提供了类似volatile的内存语义,并且保证操作原子性,因为它是由硬件实现的。

JUC中的Lock底层就是使用volatile加上CAS的方式实现的。synchronized也会尝试用cas操作来优化器重量级锁。

了解这些关键字是很有必要的。

 

四:JUC包

在了解完上述内容以后,就可以看看JUC的内容了。

JUC提供了包括Lock,原子操作类,线程池,同步容器,工具类等内容。

这些类的基础都是AQS,所以了解AQS的原理是很重要的。

除此之外,还可以了解一下Fork/Join,以及JUC的常用场景,比如生产者消费者,阻塞队列,以及读写容器等。

 

五:实践

上述这些内容,除了JMM部分的内容比较不好实现之外,像是多线程基本使用,JUC的使用都可以在代码实践中更好地理解其原理。多尝试一些场景,或者在网上找一些比较经典的并发场景,或者参考别人的例子,在实践中加深理解,还是很有必要的。

 

 

六:补充

由于很多Java新手可能对并发编程没什么概念,在这里放一篇不错的总结,简要地提几个并发编程中比要重要的点,也是比较基本的点吗,算是抛砖引玉,开个好头,在大致了解了这些基础内容以后,才能更好地开展后面详细内容的学习。

 

技术图片

1.并发编程三要素

  • 原子性
    原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  • 有序性
    程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
  • 可见性
    当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。

2. 线程的五大状态

  • 创建状态
    当用 new 操作符创建一个线程的时候
  • 就绪状态
    调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度
  • 运行状态
    CPU 开始调度线程,并开始执行 run 方法
  • 阻塞状态
    线程的执行过程中由于一些原因进入阻塞状态
    比如:调用 sleep 方法、尝试去得到一个锁等等??
  • 死亡状态
    run 方法执行完 或者 执行过程中遇到了一个异常

3.悲观锁与乐观锁

  • 悲观锁:每次操作都会加锁,会造成线程阻塞。
  • 乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。?

4.线程之间的协作

4.1 wait/notify/notifyAll

这一组是 Object 类的方法
需要注意的是:这三个方法都必须在同步的范围内调用?

  • wait
    阻塞当前线程,直到 notify 或者 notifyAll 来唤醒????

    wait有三种方式的调用
    wait()
    必要要由 notify 或者 notifyAll 来唤醒????
    wait(long timeout)
    在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。
    wait(long timeout,long nanos)
    本质上还是调用一个参数的方法
    public final void wait(long timeout, int nanos) throws InterruptedException 
          if (timeout < 0) 
                 throw new IllegalArgumentException("timeout value is negative");
           
          if (nanos < 0 || nanos > 999999) 
                  throw new IllegalArgumentException(
                 "nanosecond timeout value out of range");
           
           if (nanos > 0) 
                 timeout++;
           
           wait(timeout);
    
                  ?
    复制代码
    • notify
      只能唤醒一个处于 wait 的线程
    • notifyAll
      唤醒全部处于 wait 的线程
      ?

4.2 sleep/yield/join

这一组是 Thread 类的方法

  • sleep
    让当前线程暂停指定时间,只是让出CPU的使用权,并不释放锁

  • yield
    暂停当前线程的执行,也就是当前CPU的使用权,让其他线程有机会执行,不能指定时间。会让当前线程从运行状态转变为就绪状态,此方法在生产环境中很少会使用到,???官方在其注释中也有相关的说明

          /**
          * A hint to the scheduler that the current thread is willing to yield
          * its current use of a processor. The scheduler is free to ignore this
          * hint.
          *
          * <p> Yield is a heuristic attempt to improve relative progression
          * between threads that would otherwise over-utilise a CPU. Its use
          * should be combined with detailed profiling and benchmarking to
          * ensure that it actually has the desired effect.
          *
          * <p> It is rarely appropriate to use this method. It may be useful
          * for debugging or testing purposes, where it may help to reproduce
          * bugs due to race conditions. It may also be useful when designing
          * concurrency control constructs such as the ones in the
          * @link java.util.concurrent.locks package.
          */??
          ????
    复制代码
  • join
    等待调用 join 方法的线程执行结束,才执行后面的代码
    其调用一定要在 start 方法之后(看源码可知)?
    使用场景:当父线程需要等待子线程执行结束才执行后面内容或者需要某个子线程的执行结果会用到 join 方法?

5.valitate 关键字

5.1 定义

java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

valitate是轻量级的synchronized,不会引起线程上下文的切换和调度,执行开销更小。

5.2 原理

1. 使用volitate修饰的变量在汇编阶段,会多出一条lock前缀指令
2. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
3. 它会强制将对缓存的修改操作立即写入主存
4. 如果是写操作,它会导致其他CPU里缓存了该内存地址的数据无效

5.3 作用

内存可见性
多线程操作的时候,一个线程修改了一个变量的值 ,其他线程能立即看到修改后的值
防止重排序
即程序的执行顺序按照代码的顺序执行(处理器为了提高代码的执行效率可能会对代码进行重排序)

并不能保证操作的原子性(比如下面这段代码的执行结果一定不是100000)

    public class testValitate 
    public volatile int inc = 0;
    public void increase() 
        inc = inc + 1;
    
    public static void main(String[] args) 
        final testValitate test = new testValitate();
        for (int i = 0; i < 100; i++) 
            new Thread() 
                public void run() 
                    for (int j = 0; j < 1000; j++)
                        test.increase();
                
            .start();
        
        while (Thread.activeCount() > 2)   //保证前面的线程都执行完
            Thread.yield();
        
        System.out.println(test.inc);
     
   
复制代码

6. synchronized 关键字

确保线程互斥的访问同步代码

6.1 定义

synchronized 是JVM实现的一种锁,其中锁的获取和释放分别是
monitorenter 和 monitorexit 指令,该锁在实现上分为了偏向锁、轻量级锁和重量级锁,其中偏向锁在 java1.6 是默认开启的,轻量级锁在多线程竞争的情况下会膨胀成重量级锁,有关锁的数据都保存在对象头中

6.2 原理

加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令(利用javap -verbose 字节码文件可看到关,关于这两条指令的文档如下:

  • monitorenter
    Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
    • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
    • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
    • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor‘s entry count is zero, then tries again to gain ownership.?

  • monitorexit
    The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
    The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.??

加了 synchronized 关键字的方法,生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

6.3 关于使用

  • 修饰普通方法
    同步对象是实例对象
  • 修饰静态方法
    同步对象是类本身
  • 修饰代码块
    可以自己设置同步对象?

6.4 缺点

会让没有得到锁的资源进入Block状态,争夺到资源之后又转为Running状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。Java1.6为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

7. CAS

AtomicBoolean,AtomicInteger,AtomicLong以及 Lock 相关类等底层就是用 CAS实现的,在一定程度上性能比 synchronized 更高。

7.1 什么是CAS

CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

7.2 为什么会有CAS

如果只是用 synchronized 来保证同步会存在以下问题

synchronized 是一种悲观锁,在使用上会造成一定的性能问题。在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。一个线程持有锁会导致其它所有需要此锁的线程挂起。

 

7.3 实现原理

Java不能直接的访问操作系统底层,是通过native方法(JNI)来访问。CAS底层通过Unsafe类实现原子性操作。

7.4 存在的问题

  • ABA问题
    什么是ABA问题?比如有一个 int 类型的值 N 是 1
    此时有三个线程想要去改变它:
    线程A ??:希望给 N 赋值为 2
    线程B: 希望给 N 赋值为 2
    线程C: 希望给 N 赋值为 1??
    此时线程A和线程B同时获取到N的值1,线程A率先得到系统资源,将 N 赋值为 2,线程 B 由于某种原因被阻塞住,线程C在线程A执行完后得到 N 的当前值2
    此时的线程状态
    线程A成功给 N 赋值为2
    线程B获取到 N 的当前值 1 希望给他赋值为 2,处于阻塞状态
    线程C获取当好 N 的当前值 2 ?????希望给他赋值为1
    ??
    然后线程C成功给N赋值为1
    ?最后线程B得到了系统资源,又重新恢复了运行状态,?在阻塞之前线程B获取到的N的值是1,执行compare操作发现当前N的值与获取到的值相同(均为1),成功将N赋值为了2。
    ?
    在这个过程中线程B获取到N的值是一个旧值??,虽然和当前N的值相等,但是实际上N的值已经经历了一次 1到2到1的改变
    上面这个例子就是典型的ABA问题?
    怎样去解决ABA问题
    给变量加一个版本号即可,在比较的时候不仅要比较当前变量的值 还需要比较当前变量的版本号。Java中AtomicStampedReference 就解决了这个问题
  • 循环时间长开销大
    在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

CAS只能保证一个共享变量的原子操作

8. AbstractQueuedSynchronizer(AQS)

AQS抽象的队列式同步器,是一种基于状态(state)的链表管理方式。state 是用CAS去修改的。它是 java.util.concurrent 包中最重要的基石,要学习想学习 java.util.concurrent 包里的内容这个类是关键。 ReentrantLock?、CountDownLatcher、Semaphore 实现的原理就是基于AQS。想知道他怎么实现以及实现原理 可以参看这篇文章www.cnblogs.com/waterystone…

9. Future

在并发编程我们一般使用Runable去执行异步任务,然而这样做我们是不能拿到异步任务的返回值的,但是使用Future 就可以。使用Future很简单,只需把Runable换成FutureTask即可。使用上比较简单,这里不多做介绍。

10. 线程池

如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。

Java中线程池的实现类 ThreadPoolExecutor,其构造函数的每一个参数的含义在注释上已经写得很清楚了,这里几个关键参数可以再简单说一下

  • corePoolSize :核心线程数即一直保留在线程池中的线程数量,即使处于闲置状态也不会被销毁。要设置 allowCoreThreadTimeOut 为 true,才会被销毁。
  • maximumPoolSize:线程池中允许存在的最大线程数
  • keepAliveTime :非核心线程允许的最大闲置时间,超过这个时间就会本地销毁。
  • workQueue:用来存放任务的队列。
    • SynchronousQueue:这个队列会让新添加的任务立即得到执行,如果线程池中所有的线程都在执行,那么就会去创建一个新的线程去执行这个任务。当使用这个队列的时候,maximumPoolSizes一般都会设置一个最大值 Integer.MAX_VALUE
    • LinkedBlockingQueue:这个队列是一个无界队列。怎么理解呢,就是有多少任务来我们就会执行多少任务,如果线程池中的线程小于corePoolSize ,我们就会创建一个新的线程去执行这个任务,如果线程池中的线程数等于corePoolSize,就会将任务放入队列中等待,由于队列大小没有限制所以也被称为无界队列。当使用这个队列的时候 maximumPoolSizes 不生效(线程池中线程的数量不会超过corePoolSize),所以一般都会设置为0。
    • ArrayBlockingQueue:这个队列是一个有界队列。可以设置队列的最大容量。当线程池中线程数大于或者等于 maximumPoolSizes 的时候,就会把任务放到这个队列中,当当前队列中的任务大于队列的最大容量就会丢弃掉该任务交由 RejectedExecutionHandler 处理。

最后,本文主要对Java并发编程开发需要的知识点作了简单的讲解,这里每一个知识点都可以用一篇文章去讲解,由于篇幅原因不能对每一个知识点都详细介绍,我相信通过本文你会对Java的并发编程会有更近一步的了解。如果您发现还有缺漏或者有错误的地方,可以在评论区补充,谢谢。

 

微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站。(关注公众号后回复”Java“即可领取 Java基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Java学习指南、Java程序员面试指南等干货资源)

 

 

 

技术图片

 

 

 

以上是关于JAVA多线程并发编程-避坑指南的主要内容,如果未能解决你的问题,请参考以下文章

jmeter避坑指南

Java并发指南开篇:Java并发编程学习大纲

《java多线程编程核心技术》和《java并发编程的艺术》两本书的异同

Java修炼之道--并发编程

深入浅出Java并发编程指南「原理分析篇」底层角度去分析线程的实现原理

Java多线程编程模式实战指南:Immutable Object模式