《Java核心技术》学习笔记——第12章 并发

本文详细介绍了Java中的线程概念、线程状态、线程属性,包括线程的创建、中断、守护线程、线程安全的集合以及线程池的使用。重点讲解了线程同步的重要性,线程安全的集合如阻塞队列、并发散列映射的原子更新以及任务和线程池的实现,包括Future和ExecutorService。最后,探讨了异步计算的CompletableFuture及其组合使用。
摘要由CSDN通过智能技术生成

版权声明:本文为博主ExcelMann的原创文章,未经博主允许不得转载。

第12章 并发

作者:ExcelMann,转载需注明。

第12章内容目录:

  • 什么是线程
  • 线程属性
  • 线程安全的集合
  • 异步计算
  • 线程状态
  • 同步
  • 任务和线程池
  • 进程

开篇的介绍:
每个任务在一个线程中执行,如果一个程序可以同时运行多个线程,则称该程序时多线程的

多线程和多进程的区别在于:每个进程都拥有自己得一整套变量,而线程之间则共享数据。

一、什么是线程

在一个单独的线程中运行一个任务的简单过程
1)将执行这个任务的代码放在一个类的run方法,这个类要实现Runnable接口。这个接口很简单,只有一个方法,是一个函数式接口(所以可以用lambda表达式);
2)从这个Runnable构造一个Thread对象;
3)启动这个线程对象,调用start()方法;

线程池:如果有多个任务,为每个任务分别创建一个单独的线程开销会很大。实际上,都会采用一个线程池来管理已有的线程。

注意:不要直接调用Thread或者Runnable对象的run方法,因为这样只会在同一个线程中执行这个任务——而没有启动新的线程。实际上,应当调用Thread.start方法,这会创建一个执行run方法的新线程。

这一章余下的部分会介绍如何控制线程之间的交互。

二、线程状态

线程总共有6种状态:
1)New(新建)
2)Runnable(可运行)
3)Blocked(阻塞)
4)Waiting(等待)
5)Timed waiting(计时等待)
6)Terminated(终止)
下面将分别对每一种状态进行解释。

要确定一个线程的当前状态,可以调用getState方法。

1、新建线程

使用new操作符,如new Thread(r),这个线程还没有开始运行。只是一个新建的状态。

2、可运行线程

  1. 一旦调用start方法,线程就处于可运行状态。
  2. 注意:一个可运行状态的线程可能正在运行也可能没有运行。要由操作系统为线程提供具体的运行时间;
  3. 一旦一个线程开始运行,它不一定始终保持运行;
  4. 抢占式调度系统:给每一个可运行线程一个时间片来执行任务。当时间片用完时,操作系统会剥夺该线程的运行权,并给另一个线程一个机会来运行(选择下一个线程的时候,会按照优先级来选择);
  5. 现在所有的桌面以及服务器操作系统都使用抢占式调度。

3、阻塞和等待线程

  1. 当线程处于阻塞或等待状态时,它暂时是不活动的。它不会运行任何代码,也消耗最少的资源;
  2. 需要由线程调度器重新激活这个线程。具体的激活细节取决于它是怎么到达非活动状态的;
  3. 到达非活动状态的几种情况
    1)当一个线程试图获取一个内部的对象锁(而不是concurrent库中的Lock),而这个锁目前被其他线程占有,该线程就会被阻塞;当没有线程占有这个锁,而且线程调度器允许该线程持有这个锁时,它才会变成非阻塞状态;
    2)当线程等待另一个线程通知调度器(条件对象情况)出现一个条件时,这个线程会进入等待状态;
    3)有几个方法有超时参数,调用这些方法会让线程进入计时等待状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep和计时版的Object.wait、Condition.await等方法;
  4. 线程重新激活后的运行原理:当一个线程被重新激活后,调度器会检查它是否具有比当前运行线程更高的优先级。如果是这样,调度器会剥夺某个当前运行线程的优先权,选择一个新线程运行;

4、终止进程

线程会由于以下两个原因之一而终止:
1)run方法正常退出,线程自然终止;
2)因为一个没有捕获的异常终止了run方法,使线程意外终止;

三、线程属性

1、中断线程

  1. 线程终止的原因:当run方法执行方法体中最后一条语句后再执行return语句返回时,或者出现了未捕获的异常时,线程将终止;
  2. interrupt方法:该方法可以用来请求终止一个线程。当对一个线程调用interrupt方法时,就会设置线程的中断状态。这是每个线程都有的boolean状态,每个线程都应该不时地检查这个标志,以判断线程是否要求被中断;
  3. 判断是否设置了中断状态(isInterrupted方法):要想得出是否设置了中断状态,首先调用静态的Thread.currentThread方法获得当前线程,然后调用isInterrupted方法判断是否处于中断状态;
  4. InterruptedException:该异常有以下两个作用:
    1)当线程阻塞时,如何检查有中断请求:如果线程被阻塞,就无法检查中断状态。这里需要引入该异常。当在一个sleep或wait调用阻塞的进程上调用interrupt方法时,该阻塞调用会被一个InterruptedException异常中断;
    2)当循环中调用了sleep时,如何检查是否中断:在中断状态时调用了sleep或者其他阻塞方法时,会清除中断状态并且抛出该异常,如果你的代码循环调用了sleep,不要检查中断状态,而应当捕获该异常;
  5. 中断不等于终止:中断一个线程只是要引起它的注意。被中断的线程可以决定如何响应中断。如果某个线程非常重要,就应该处理这个异常;
  6. interrupted和isInterrupted的区别:前者是一个静态方法,它检查当前线程是否被中断,而且,调用该方法会清除该线程的中断状态;后者是一个实例方法,可以用来检查是否有线程被中断,调用该方法不会清除线程的中断状态;
  7. 不要抑制InterruptedException异常:如果想不出在catch子句中可以做什么有意义的事情,仍然有两种合理的处理方式:
    1)在catch子句中调用Thread.currentThread().interrupt()来设置中断状态。这样一来调用者就可以检测到中断状态;
    2)用throws.InterruptedException标记你的方法,去掉try语句块。这样一来调用者就可以捕获到这个异常;

2、守护线程

setDaemon方法:可以通过调用该方法,将一个线程转换为一个守护线程(该方法必须在线程启动之前调用);

用途:守护线程的唯一用途是为其他线程提供服务;

例子:计时器线程就是一个例子,它定时地发送“计时器滴答”信号给其他线程,另外清空过时缓存项的线程也是守护线程;

注释:当只剩下守护线程时,虚拟机就会退出。因为只剩下守护线程,就没必要继续运行程序了;

3、线程名

可以使用setName方法为线程设置任何名字;

4、未捕获异常的处理器

以下提的处理器,均为未捕获异常的处理器。

  1. 处理未捕获异常的处理器:实际上,在线程死亡之前,异常会传递到一个用于处理未捕获异常的处理器。
    这个处理器必须属于一个实现了Thread.UncaughtExceptionHandler接口的类;
//该接口只有一个方法,当线程因一个未捕获异常而终止时,要记录一个定制报告
void uncaughtException(Thread t, Throwable e)
  1. 为线程安装处理器
    1)可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器;
    2)为所有线程安装默认处理器:用Thread.setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器;
  2. 没有安装处理器的情况:如果没有为线程安装默认处理器,那么默认处理器则为null;如果没有为单个线程安装处理器,那么处理器就是该线程的ThreadGroup对象;
  3. ThreadGroup:线程组是可以一起管理的线程的集合。默认情况下,创建的所有线程都属于同一个线程组,但是也可以创建新的线程组。
    该类实现了Thread.UncaughtExceptionHandler接口。
    它的uncaughtExcepion方法执行以下操作
    1)如果该线程组有父线程组,那么调用父线程组的uncaughtException方法;
    2)否则,如果Thread.getDefaultExceptionHandler方法返回一个非null的处理器,则调用该处理器;
    3)否则,如果Throwable是ThreadDeath的一个实例,什么都不做;
    4)否则,将线程的名字以及Throwable的栈轨迹输出到System.err;

5、线程优先级

  1. 可以用setPriority方法设置线程的优先级;
  2. 优先级的类别:MIN_PROIRITY(定义为1)与MAX_PRIORITY(定义为10)之间的任何值。NORM_PRIORITY定义为5;
  3. 注意:线程的优先级高度依赖于系统。当Java虚拟机依赖于宿主机平台的线程实现时,Java线程的优先级会映射到宿主机平台的优先级,所以会存在不同的情况;
  4. 最好不要使用线程优先级了;

四、同步

该节内容较多,且很重要,所以用写在了另一篇文章中~
【原创】Java并发同步知识——《Java核心技术》

五、线程安全的集合(★★★)

可以通过提供锁来保护共享的数据结构,但是选择线程安全的实现可能更为容易。在下面各小节中,将讨论Java类库提供的另外一些线程安全的集合。

1、阻塞队列(BlockingQueue)

  1. 例子说明:例如,考虑银行转账程序,转账线程将转账指令对象插入一个队列,而不是直接访问银行对象。另一个线程从队列中取出指令完成转账。故,只有这个线程可以访问银行对象的内部,因此不需要同步。
  2. 阻塞队列实现自动平衡负载:如果第一组线程运行的比第二组慢,第二组在等待结果时会阻塞。如果第一组线程运行的更快,队列会填满,直到第二组赶上来。
  3. 阻塞队列的方法
    1)如果使用队列作为线程管理工具,将要用到put和take方法。如果队列满或者队列空,这两个方法会阻塞;
    2)应该是用offer、poll和peek而不是add、remove和element。前者如果不能完成任务,只是会给出一个错误提示而不会抛出异常,而后者会直接抛出异常;
    3)还有带有超时时间的offer和poll方法:如果在限定时间内完成操作,则正常返回值,如果没能在限定时间内完成操作,则返回false和null;
  4. java.util.concurrent包提供的:该包提供了阻塞队列的几个变体。
    1)LinkedBlockingQueue
    2)LinkedBlockingDeque:双端队列
    3)ArrayBlockingQueue
    4)PriorityBlockingQueue:优先队列
    5)DelayQueue:元素只有在延迟结束的情况下才能从DelayQueue中移出
    6)TransferQueue接口和LinkedTransferQueue类:Java7新增的,后者的类实现了前者的接口

2、高效的映射、集和队列(主要讲映射)

  1. java.util.concurrent包提供的:该包提供了映射、有序集和队列的高效实现:ConcurrentHashMapConcurrentSkipListMapConcurrentLinkedQueue
    特点:这些集合使用复杂的算法,通过运行并发访问数据结构的不同部分尽可能减少竞争;
  2. 与其他大多数集合不同:这些类的size方法不一定在常量时间内完成操作。确定这些集合的当前大小通常需要遍历;
  3. 特殊情况的size变体为mappingCount方法:有些应用使用庞大的并发散列映射,这些映射太过庞大,以至于无法用size方法得到它的大小,因为这个方法只能返回int。而mappingCount方法可以把大小作为long返回;
  4. 返回弱一致性的迭代器:这些集合会返回弱一致性的迭代器。这意味着迭代器不一定能反映出它们构造之后的所有更改,但是,它们不会将同一个值返回两次,也不会抛出ConcurrentModificationException异常;
  5. 并发散列映射可以高效地支持大量阅读器和一定数量的书写器;
  6. 新版本Java的并发散列映射实现:较新版本的Java中,并发散列映射将桶组织为树,而不是列表,键类型实现Comparable,从而可以保证性能为log(n);

3、映射条目的原子更新

  1. 线程安全的集合≠解决了同步问题:例如下面这个例子,考虑让计数自增的代码,显然,下面的代码不是线程安全的。
    因为可能会有另一个线程在同时更新一个计数。
Long oldValue = map
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值