美文网首页
Java并发

Java并发

作者: 往返于无声 | 来源:发表于2020-03-10 18:58 被阅读0次

三种线程池

  • CachedThreadPool

    corePoolSize=0MaximumPoolSize=Interger.MAX_VALUE

  • FixedThreadPool

    使用无界队列 LinkedBlockingQueue,队列长度 Integer.MAX_VALUE 。只有 固定 的线程数量。

  • SingleThreadPool

    corePoolSizeMaximumPoolSize 都为1,队列长度 Integer.MAX_VALUE

使用线程池的好处

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的资源消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池大小的确定

如果线程池数量太小,将会导致大量任务在任务队列中等待执行,甚至出现队列满了,无法请求处理的情况,大大降低CPU的利用率。
线程池数量太大,会同时有太多线程在执行,导致上下文切换开销增大,影响整体的执行效率。

CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

线程池创建方法

  1. 通过构造方法实现

    ThreadPoolExecutor(int corePoolSize,
                       int maximumPoolSize,
                       long keepAliveTime,
                       TimeUnit unit,
                       BlockingQueue<Runnable> workQueue,
                       ThreadFactory threadFactory,
                       RejectedExecutionHandler handler) 
    
  1. 通过Executor 框架的工具类Executors来实现

    • FixedThreadPool

    • SingleThreadExecutor

    • CachedThreadPool

ThreadPoolExecutor 重要参数

  1. corePoolSize

    核心线程数线程数定义了最小可以同时运行的线程数量

  2. maximumPoolSize

    当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数

  3. workQueue

    当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

  4. keepAliveTime

    当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。

  5. unit

    keepAliveTime 参数的时间单位

  6. threadFactory

    用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。

  7. handler

    线城池的饱和策略事件,主要有四种类型。

关键字

  1. synchronized

    1. 代码块形式

      synchronized(this){
        //code
      }
      
    2. 方法形式

      // 修饰普通方法,给当前实例加锁
      public synchronized void method(){}
      // 修饰静态方法,给当前类加锁
      public static synchronized void method() {}
      
    • 对象锁

      修饰普通方法

    • 类锁

      修饰静态方法

保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

  • 偏向锁

    这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。

    锁膨胀

    假如有两个线程来竞争该锁话,那么偏向锁就失效了,进而升级成轻量级锁了。

  • 轻量级锁 (乐观锁)

    • 自旋锁

      就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。

      缺点 消耗CPU 优点 节省上下文切换的开支

    • 自适应自旋锁

      自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。

      切换为重量级锁

      轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。但是如果同步代码块执行速度慢,则会在原地等待锁上浪费CPU资源。

      必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁

  • 重量级锁 (悲观锁)

    当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗CPU。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从 用户态 转换到 内核态 ,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

  1. volatile

    • 可见性

      在多线程环境下,某个共享变量如果被其中一个线程给修改了,其他线程能够立即知道这个共享变量已经被修改了,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的 工作空间 中读取。嗅探内部缓存中的内存地址在其他处理器的操作情况,若发生变化,则令缓存中的数据失效。

    • 有序性

      防止指令重排。虚拟机会保证这个变量之前的代码一定会比它先执行,而之后的代码一定会比它慢执行。

    结论

    volatile 解决多线程内存不可见问题,但无法保证线程安全因为Java的运算操作并非原子操作。int a = b + 1; b+1执行完没写入内存就切换线程就会导致线程不安全问题。

    对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。

    volatile是不安全的。因为java中的操作并不是 原子操作,取数、操作、存数,所以使用volatile 不能保证线程安全。

线程池的关闭

[图片上传失败...(image-2b6b48-1583837855247)]

  • shutDown()

    关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务,但是队列里的任务会执行完毕。调用后,isShutDown() 变为True, 之后待所有提交的任务完成后 isTerminated() 返回为 True。

  • shutDownNow()

    关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,停止处理排队的任务并返回正在等待执行的 List。

线程池的执行流程(原理)

[图片上传失败...(image-beb044-1583837855247)]

四种拒绝策略

  1. AbortPolicy (抛出一个异常,默认的)
  2. DiscardPolicy (直接丢弃任务)
  3. DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
  4. CallerRunsPolicy(交给线程池,调用所在的线程进行处理)

Runnable 和 Callable

Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

execute() 和 submit() 的区别

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否; Runnable

  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功。Callable

    可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

Atomic 原子类

Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

JUC(java.util.concurrent)中的原子类

  1. 基本类型

    • AtomicInteger 整形原子类

      AtomicInteger 类主要利用 CASvolatilenative方法 来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

    • AtomicLong 长整型原子类

    • AtomicBoolean 布尔型原子类

  2. 数组类型

    • AtomicIntegerArray 整形数组原子类
    • AtomicLongArray 长整形数组原子类
    • AtomicReferenceArray 引用类型数组原子类
  3. 引用类型

    • AtomicReference 引用类型原子类
    • AtomicStampedReference 原子更新引用类型里的字段原子类
    • AtomicMarkableReference 原子更新带有标记位的引用类型
  4. 对象的属性修改类型

    • AtomicIntegerFieldUpdater 原子更新整形字段的更新器
    • AtomicLongFieldUpdater 原子更新长整形字段的更新器
    • AtomicStampedReference 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

AQS (AbstractQueuedSynchronizer)

AQS是一个 用来构建同步器 的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器。

ScheduledThreadPoolExecutor

待补充

面试题

  1. start()run() 的区别

用start方法来启动线程才真正实现了多线程运行。在调用start方法后,开启了一个新的线程,此时新线程处于就绪状态,等到获取cpu事件片才可以运行;用run方法只是调用了一个普通方法,没有开启新线程,程序仍然在主线程下运行。

Ps 本文部分摘录自JavaGuide https://gitee.com/SnailClimb/JavaGuide/tree/master

相关文章

网友评论

      本文标题:Java并发

      本文链接:https://www.haomeiwen.com/subject/ywugdhtx.html