likes
comments
collection
share

【Java常见问题】基础知识(六)多线程

作者站长头像
站长
· 阅读数 12

并发程序设计是操作系统层面的概念,而许多高级语言都有在语言层面为这种机制提供相应的支持。在这篇文章中我们一起来学习一下Java的多线程。

实现Java多线程的方式?

  • 通过Thread对象的API控制线程
    • extends Thread类,重写run方法
    • 实现Runnable接口,实现run方法(推荐)

一个类是否可以同时extends Thread和实现Runnable接口?

可以。只要是这两个的其中一个提供了run()方法所以不导致冲突就行。

run与start的区别? run可以类比为操作系统进程概念中的运行态,而start是就绪态。

  • 通过Executor框架中的功能类Callable对象

    • 实现Callable接口,重写call()方法
  • 使用线性池

线程池

Executor接口

以下是官方文档对该接口作用的描述:

【Java常见问题】基础知识(六)多线程

ExecutorService

ExecutorService提供了一系列对Executor生命周期的管理。

文档中提到的内存一致性影响:在将 Runnable 对象提交到执行程序之前,线程中的操作在其执行开始之前已经发生,可能在另一个线程中发生。

【Java常见问题】基础知识(六)多线程

ThreadPoolExecutor

笔者找到的最全的构造方法中包含如下参数: 【Java常见问题】基础知识(六)多线程

其中:

  • corePoolSize - the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set 线程池的基本大小
  • maximumPoolSize - the maximum number of threads to allow in the pool 线程池的最大容量
  • keepAliveTime - when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating. 过剩的线程的过期时限
  • unit - the time unit for the keepAliveTime argument 线程过期时限的最小计量单位
  • workQueue - the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method. 用于存放被execute方法提交的就绪任务的队列
  • threadFactory - the factory to use when the executor creates a new thread 执行者创建线程时所用到的工厂
  • handler - the handler to use when execution is blocked because the thread bounds and queue capacities are reached 当执行被阻塞时(如到达线程数量上限)所用到的句柄
线程池工作流程?
  • 若现有线程数少于限额corePoolSize,直接创建新线程以执行任务
  • 若大于限额,先暂时把任务存到workQueue中等待执行
  • 若workQueue也满,若线程数量小于maxinumPoolSize,创建新线程;否则拒绝处理。

Executors类中四种常用的线程池应用

【Java常见问题】基础知识(六)多线程 【Java常见问题】基础知识(六)多线程 【Java常见问题】基础知识(六)多线程

实现多线程同步的具体方式?

  • synchronized
    • 方法声明前加入
    • 代码块{}为synchronized
  • wait()与notify() 在synchronized代码块执行期间,线程可通过wait()进入等待状态,且通过notify()去通知其他线程竞争锁。

wait和notify是由虚拟机本地的c代码实现的。

wait与sleep的区别?

  • sleep()可理解为是线程针对自身的管理机制;而wait()则涉及到进(线)程间通信,与notify()/notifyall搭配使用,需要由其他线程唤醒或开发人员设定规定时间后醒来。
  • 调用sleep不会释放锁,调用wait会
  • sleep使用场景更通用,比如单纯想让程序慢一些;wait有并发编程的特殊语境。
  • sleep需要捕获异常,wait-notify不需要

sleep()与yield()的区别?

yield有放弃、让出之意。因此它和sleep在语义上就有很大不同:

  • 进入sleep的线程对应于操作系统中的阻塞态(等待态),而yield的依然很活跃,只是重新进入了就绪态。
  • 一个线程sleep后,其他线程无论优先级高低都有机会;而yield只是给相同或更高优先级的线程让步。
  • sleep声明会抛出异常,yield不会。
  • sleep的可移植性更好
  • Lock

回顾:操作系统中造成死锁需要同时满足哪几个条件?

  • 互斥
  • 不可剥夺
  • 已经有了资源,又去申请别的资源,但别的资源一直得不到释放,自己也不愿意释放现有的资源
  • 环路等待

synchronized与lock的异同?

synchronizedlock
锁机制自动释放,获取多个锁时先进的后出手动在finally块中释放,tryLock()可以非阻塞方式获取锁
性能低并发时syn优于lock,高并发时syn性能下降快JDK5开始的ReentranLock性能相对稳定

最值得关注的Java Lock

  • ReentrantLock(重入锁、递归锁)
  • ReentrantReadWriteLock 将锁进一步细分为读、写锁以提高并发程度

Java代码中如何终止线程?

官方已经不太推荐使用的方法:

  • stop

执行stop的线程会释放相应资源,而受资源保护的对应对象的状态不一致且被其他线程感知,可能会导致程序执行的不确定性,且难以排查。

  • suspend

suspend不释放锁,容易导致死锁

参考思路:

基本思路就是让线程离开run(),自行进入Dead状态。

而如何让它离开又分具体情况。

  • 对症下药

比如,使用readLine等待输入信息,而线程处于阻塞状态,为了让它离开run(),可以close掉对应的流,从而引发IOException,让run()捕获异常而结束线程。(一般是针对处于非运行态的线程来进行对症处理)

  • 使用中断

通过使用中断来抛出InterruptedException,让run()捕获异常而结束线程。但这种方法仅适用于程序处在运行态时。

如何捕获线程抛出的异常?

线程抛出的异常无法通过try-catch捕获(线程是什么?它也是由独立的代码片段组成的)。但从JDK5开始,官方提供了Thread.UncaughtExceptionHandler接口,可以实现其中的uncaughtException()方法并调用。

其他可关注的应用点

ThreadLocal

  • 应用场景:多线程环境下,某一数据可一直都由其中一个线程访问

以下是官方文档对ThreadLocal的介绍:

【Java常见问题】基础知识(六)多线程 如何理解呢?顾名思义,我认为是这个值与特定的线程绑定了(比如通过用户id、事务id等),单个线程修改的是自己本地可见的值的变量,不影响其他线程对应的变量。不同的线程读到的变量的值是不同的。

实现原理

  • 线程的对象存储在堆中,对象的引用存储在栈中。
  • 每个线程内部维护一个ThreadLocalMap,key是对ThreadLocal的引用,value存储了真正的对象。
  • 当ThreadLocal的set/get被调用时,JVM会根据栈中线程的引用ThreadRef找到它在堆中的实例,若对应的map实例未被创建则创建,否则利用当前的key进行存取操作。

ThreadLocal为什么会导致内存泄露?

因为key是弱引用,在GC时可能会被回收,导致对应value无法被访问。但是Entry本身无法被回收,很多线程长期存在,导致了内存的泄露。

值得留意的特性

jdk8开始新增了ThreadLocal的一个方法:

【Java常见问题】基础知识(六)多线程

CountDownLatch

  • 应用场景: 【Java常见问题】基础知识(六)多线程 直译就是:让一个或一组线程等待指定的计数时间,直到另一组线程完成了所需要执行的一系列操作之后。
  • 下面这一段指出了三大关键信息: 【Java常见问题】基础知识(六)多线程
    • 计数期间,处于阻塞状态的是await()方法,因此这后续的代码要等指定任务执行完后才执行。
    • 计数器不可重用
    • 如有需要可以用CyclicBarrier

CyclicBarrier

【Java常见问题】基础知识(六)多线程

官网的示例代码也很详细,感兴趣的uu可以看看。

ForkJoinPool

Fork-Join是自JDK7以来的一个高效的小工具,顾名思义,具有和MapReduce类似的特点:先将任务拆解开来分别完成,最后再合并,以提高执行效率。

以下是关于它的两个具体应用的类:

ForkJoinTask

【Java常见问题】基础知识(六)多线程 从上述文字中可获取以下信息:

  • 它是一个轻量级的线程,可看作是ForkJoinPool中执行的task。
  • 它主要的方法有:fork(),join(),invote()/invoteAll()
  • 它实现了Future接口。

【Java常见问题】基础知识(六)多线程 由上述描述可知:

  • 它有两个子类:RecursiveAction不返回结果;RecursiveTask返回结果

ForkJoinPool

【Java常见问题】基础知识(六)多线程 上述内容揭示了ForkJoin不同于其他执行任务的特性:work-stealing机制。指的是线程把自己工作队列中的任务完成好了以后,还会偷偷地把别的线程的任务也做了(抢着干活)

【Java常见问题】基础知识(六)多线程 上述内容表明: 有两种池可选:commonPool和ForkJoinPool。

  • 前者在大多数情况下使用,但是资源利用率会比较低(因为线程不被使用时被回收得慢);
  • 针对需要指定或自定义池的应用,可以将ForkJoinPool构造成指定的并行级别。

两者的调用方法写法也是略有不同的: 【Java常见问题】基础知识(六)多线程 下图表明:ForkJoinPool支持同步和异步两种方式。默认的是同步,管理工作线程使用的是栈形式;而异步使用的是队列形式。 【Java常见问题】基础知识(六)多线程

关于CAS(compare and swap)

算法原理

以下是伪代码:

function cas(p : pointer to int, old : int, new : int) returns bool {
    if *p ≠ old {
        return false
    }
    *p ← new
    return true
}

即修改值的时候,先判断它有没有被修改过,如果没有被修改过则修改它并返回成功;否则返回失败。它是用于多线程实现同步的原子命令。

几大问题

  • 原子问题涉及到CPU指令顺序问题,Java能否控制到这一级别?

Java有一个非公开的类sun.misc.Unsafe,封装了一系列原子化操作,不过这些方法本质上也是由C/C++实现的。

  • 多线程环境下如何保证结点交换时的线程安全?

要回答这一问题,就是要回答xx方法如何保证线程安全的三大特性:原子性、有序性、可见性。

AbstractQueuedSynchronizer(AQS)使用CAS机制保障原子性,使用volatile保证head和tail结点在执行中的有序性可见性

  • 如何解决CAS引起的ABA漏洞问题?

ABA漏洞问题,指的是由于若同一个值在前后阶段相同,CAS则认为它没有被修改,但实际上可能已经被修改。比如当一个栈中有BA两个元素(A为栈顶),过程中另一个线程将AB出栈,又将CA入栈,则原来的线程再次读取值时还以为栈未被修改,它想将B变成栈顶就会出错。

解决方案是留有一个“版本号”,每次更新数据后版本号+1,以此比对数据是否被修改过。