多线程并发
2021-11-25 18:55:37 35 举报
AI智能生成
多线程总结(给个赞吧)
作者其他创作
大纲/内容
进阶
CompletionService
作用
是以 异步 的 方式 一边 生产 新的 任务, 一边 处理 已完成 任务 的 结果, 这样 可以 将 执行任务 与 处理 任务 分离 开来 进行 处理。 使用 submit 执行任务, 使用 take 取得 已完成 的 任务, 并按 照 完成 这些 任务 的 时间 顺序 处理 它们 的 结果。
实现类
ExecutorCompletionService
方法
方法 take() 取得 最先 完成任务 的 Future 对象, 谁 执行 时间 最短 谁 最先 返回。
方法 poll() 的 作用 是 获取 并 移 除 表示 下一个 已完成 任务 的 Future, 如果不 存在 这样 的 任务, 则 返回 null, 方法 poll() 无 阻塞 的 效果。
方法 Future< V> poll( long timeout, TimeUnit unit) 的 作用 是 等待 指定 的 timeout 时间, 在 timeout 时间 之内 获取 到 值 时 立即 向下 继续 执行, 如果 超时 也 立即 向下 执行。
总结
接口 CompletionService 完全 可以避免 FutureTask 类 阻塞 的 缺点, 可 更加 有效地 处理 Future 的 返回 值, 也就是 哪个 任务 先 执行 完, CompletionService 就 先取 得 这个 任务 的 返回 值 再处理。
JUC同步工具类
CountDownLatch
作用
CountDownLatch允许一个或多个线程等待其他线程完成后再操作
基本用法
CountDownLatch down = new CountDownLatch( 1);创建 1 个 计数 的 CountDownLatch 类 的 对象
down. countDown(); count减1
down. await();等待,直到其它线程将count减为0时再往下执行
其他概念
方法 await( long timeout, TimeUnit unit) 的 作用 使 线程 在 指定 的 最大 时间 单位 内 进入 WAITING 状态, 如果 超过 这个 时间 则 自动 唤醒, 程序 继续 向下 运行。 参数 timeout 是 等待 的 时间, 而 unit 参数 是 时间 的 单位。
方法 getCount() 获取 当前 计数 的 值。
应用场景
开启多个线程分块下载一个大文件,每个线程只下载固定的一截,最后由另外一个线程来拼接所有的分段。
应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。
确保一个计算不会执行,直到所需要的资源被初始化。
CyclicBarrier
作用
它允许一组线程互相等待,直到到达某个公共屏障 这些线程必须实时地互相等待,这种情况下就可以使用CyclicBarrier类来方便地实现这样的功能。 CyclicBarrier类的公共屏障点可以重用, 所以类的名称中有“ cyclic 循环” 的单词。
基本用法
一组线程中每个线程调用CyclicBarrier.await()方法等待其他线程到达一个公共屏障点
new CyclicBarrier(5,Runnable),设置 最大 为 5 个 的 parties 同 行者, 也就是 5 个 线程 都 执行 了 cbRef 对象 的 await() 方法 后 程序 才可 以 继续 向下 运行, 否则 这些 线程 彼此 互相 等待, 一直 呈 阻塞 状态。
应用场景
多线程计算
和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置
CountDownLatch.await一般阻塞主线程,而CyclicBarrierton一般阻塞工作线程
CountDownLatch主要用于描述一个或者多个线程等待其他线程执行完毕
CyclicBarrier主要用于描述多个线程之间相互等待
其他概念
getNumberWaiting() , 该 方法 的 作用 是 获得 有几个 线程 已经 到达 屏障 点。
方法 isBroken() 查询 此 屏障 是否 处于 损坏 状态。
方法 isBroken() 查询 此 屏障 是否 处于 损坏 状态。由于 中断 或者 超时 提前 离开 了 屏障 点, 其他 所有 在 屏障 点 等待 的 线程 也会 抛出 BrokenBarrierException 或者 InterruptedException 异常, 并且 离开 屏障 点。
方法 await( long timeout, TimeUnit unit) 的 功能 是 如果 在 指定 的 时间 内 达到 parties 的 数量, 则 程序 继续 向下 运行, 否则 如果 出现 超时, 则 抛出 TimeoutException 异常。
方法 getParties() 的 作用 是 取得 parties 个数。
reset() 的 作用 是 重置 屏障。
Phaser
作用
Phaser表示“阶段器”,用来解决控制多个线程分阶段共同完成任务的情景问题
基本用法
extends Phaser
@override onAdvance()
应用场景
可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接
Semaphore
作用
可以控制同时访问的线程个数,它维护了一组"许可证"。控制线程并发的数量
主要用法
semaphore.acquire();//消费一个许可证。如果没有许可证了,会阻塞起来
System.out.println(Thread.currentThread().getName() + ",begin timer=" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ",end timer=" + System.currentTimeMillis());
semaphore.release();//添加一个许可证
其他概念
有参方法 acquire(int permits) 的 功能 是 每 调用 1 次 此 方法, 就使用 x 个 许可。说白了就是一共有10把锁,每进来一个人就消耗掉2把锁,所以一共只能进来5次
构造 参数 new Semaphore(5);中的 5 并不是 最终 的 许可 数量, 仅仅是初始的状态 值。比如初始是5,如果release(5),那avaliablePermits=10
方法 acquireUninterruptibly() 的 作用 是 使 等待 进入 acquire() 方法 的 线程, 不允许 被 中断。也就是正常执行不抛出异常
availablePermits() 返回 此 Semaphore 对象 中 当前 可用 的 许可 数, 此 方法 通常用于 调试, 因为 许可 的 数量 有可能 实时 在 改变, 并不是 固定 的 数量。
drainPermits() 可 获取 并 返回 立即 可用 的 所有 许可 个数, 并且 将 可用 许可 置 0。
方法 getQueueLength() 的 作用 是 取得 等待 许可 的 线程 个数。
方法 hasQueuedThreads() 的 作用 是 判断 有没有 线程 在等 待 这个 许可。
Semaphore semaphore = new Semaphore( 1, isFair);
获得 许可 的 顺序 与 线程 启动 的 顺序 有关, 这时 信号 量 就要 分为 公平 与 非 公平 的。 所谓 的 公平 信号 量 是 获得 锁 的 顺序 与 线程 启动 的 顺序 有关, 但不 代表 100% 地 获得 信号 量, 仅仅 是在 概率 上 能 得到 保证。 而非 公平 信号 量 就是 无关 的 了。
无 参 方法 tryAcquire() 的 作用 是 尝试 地 获得 1 个 许可, 如果 获取 不到 则 返回 false, 此 方法 通 常与 if 语句 结合 使用, 其 具有 无 阻塞 的 特点。 无 阻塞 的 特点 可以 使 线程 不至于 在 同步 处 一直 持续 等待 的 状态, 如果 if 语句 判断 不 成立 则 线程 会 继续走 else 语句,
有 参 方法 tryAcquire( int permits) 的 作用 是 尝试 地 获得 x 个 许可, 如果 获取 不到 则 返回 false。
有 参 方法 tryAcquire( int long timeout, TimeUnit unit) 的 作用 是在 指定 的 时间 内 尝试 地 获得 1 个 许可, 如果 获取 不到 则 返回 false。
有 参 方法 tryAcquire( int permits, long timeout, TimeUnit unit) 的 作用 是在 指定 的 时间 内 尝试 地 获得 x 个 许可, 如果 获取 不到 则 返回 false。
Exchanger
作用
Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
基本用法
两个线程分别调用ex.exchange(Object obj)方法,互相阻塞,互相得到exchange方法的返回值
其他概念
调用 exchange( V x, long timeout, TimeUnit unit) 方法 后 在 指定 的 时间 内 没有 其他 线程 获取 数据, 则 出现 超时 异常。
LookSupport
LookSupport.park()
当前线程等待
LookSupport.unpark(Thread)
唤醒Thread线程
AQS
AQS
一个抽象的队列式的同步器,定义了一套多线程访问共享资源的同步器框架(实现锁的框架),许多同步类实现都依赖于它,本质上其实就是对于state的获取和释放
ReentrantLock/ReentrantReadWriteLock /Semaphore/ CountDownLatch的实现都依赖于它
数据结构
共享资源 volatile int state
默认实现了FIFO线程等待队列,底层是双向链表
线程模式
独享模式
tryAcquire(int)/tryAcquireShared(int )
共享模式
tryRelease/tryReleaseShared(int)
既可独占,也可共享(ReentrantReadWriteLock)
并发集合框架
继承关系
Iterable
Collection
List
ArrayList
非 线程 安全
Vector
线程安全
当 多个 线程 分别 调用 该类 的 iterator() 方法 返回 Iterator 对象 后, 再 调用 remove() 时会出现 ConcurrentModificationException 异常, 也就是 并不 支持 Iterator 并发 的 删除
子类Stack
后进 先出( LIFO) 的 对象 堆栈
Queue
可以 方便 地 操作 列 头,poll,add,remove等方法
PriorityQueue
它是 一个 基于 优先级 的 无 界 优先级 队列
Deque(接口)
双端 队列,表头表尾都可以操作
ArrayDeque
队列 两端 获取 数据
LinkedList
队列 两端 获取 数据 ,还可以根据 索引 的 位置 操作 数据
Set
具有 的 默认 特点 是 内容 不允许 重复, 排序 方式 为 自然 排序, 防止 元素 重复 的 原理 是 元素 需要 重写 hashCode() 和 equals() 方法
HashSet
非线程安全,无序
LinkedHashSet
非线程安全,有序
TreeSet
它不 仅 实现 了 Set 接口, 而且还 实现 了 SortedSet 和 NavigableSet 接口
SortedSet 和 NavigableSet 接口 在 功能上 得到 了 扩展, 比如 可以 获取 Set 中 内容 的 子集, 以比 较 范围 进行 获得 子集, 支持 对 表头 与 表尾 的 数据 进行 获取 等,
Map
AbstractMap
HashMap
非线程安全
HashTable
线程安全 sycn
LinkedHashMap
ConcurrentHashMap
数据结构
JDK1.7
segment
继承了ReentrantLock
一个concurrentHashMap包含了一个segment数组
一个segment包含了一个hashEntry链表
hashEntry
链表结构的元素
hashEntry的成员变量除了value都定义为final
为了维护链表结构,防止并发问题
JDK1.8
Node数组
链表
红黑树
常用方法底层实现(JDK1.7)
put()
流程
1.首先Hash定位到Segment
2.对当前Segment加锁,如果Segment中元素的数量超过了阈值,则需要进行扩容并且进行rehash
3.再定位到链表头部
get()
不用加锁,是非阻塞的
因为共享变量都定义为了volatile
根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景
volatile能够保证内存可见性
需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
定位到链表头部之后根据key取出对应的value值
如果取出的value是null,则对取出value这一过程进行加锁(lock())
取出的value是null的原因是可能现在正在进行put操作
如果不是value,则直接返回value值
remove()
因为HashEntry中的next是final的,一经赋值以后就不可修改,所以在定位到待删除元素e的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去。尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用。e之前的元素在remove()之后为remove之前的逆置
size()
size()操作涉及到多个segment
size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了
特点
key和value都不可以为null
get()方法不加锁,是非阻塞的
是线程安全的
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
非阻塞队列
Map相关
ConcurrentHashMap
线程安全
支持并发操作
ConcurrentSkipListMap
线程安全
支持排序
支持并发操作
Set相关
ConcurrentSkipListSet
支持并发操作
支持排序
支持并发操作
不允许重复
CopyOnWriteArraySet
Queue相关
ConcurrentLinkedQueue
方法 poll() 当 没有 获得 数据 时 返回 为 null, 如果 有 数据 时 则 移 除 表头, 并将 表头 进行 返回。
方法 element() 当 没有 获得 数据 时 出现 NoSuchElementException 异常, 如果 有 数据 时 则 返回 表头 项。
方法 peek() 当 没有 获得 数据 时 返回 为 null, 如果 有 数据 时 则 不移 除 表头, 并将 表头 进行 返回。
ConcurrentLinkedDeque
双端队列
pollFirst()
pollLast()
List相关
CopyOnWriteArrayList
阻塞队列
概念
阻塞 队列 BlockingQueue, 其实 就是 如果 BlockQueue 是 空的, 从 BlockingQueue 取 东西 的 操作 将会 被 阻塞 进入 等待 状态, 直到 BlockingQueue 添加 进了 元素 才会 被 唤醒。 同样, 如果 BlockingQueue 是 满的, 也就是 没有 空余 空间 时, 试图 往 队列 中 存放 元素 的 操作 也会 被 阻塞 进入 等待 状态, 直到 BlockingQueue 里 有 剩余 空间 才会 被 唤醒 继续 操作。
分类
List相关
Set相关
Queue相关
ArrayBlockingQueue
有界
PriorityBlockingQueue
LinkedBlockingQueue
无界
类似ArrayBlockingQueue
LinkedBlockingDeque
双端操作
SynchronousQueue
一种 阻塞 队列, 其中 每个 插入 操作 必须 等待 另一个 线程 的 对应 移 除 操作, 反之亦然。 同步 队列 没有 任何 内部 容量, 甚至 连 一个 队列 的 容量 都没 有。 不能 在 同步 队列 上 进行 peek, 因为 仅在 试图 要 移 除 元素 时, 该 元素 才 存在; 除非 另一个 线程 试图 移 除 某个 元素, 否则 也不能( 使用 任何 方法) 插入 元素; 也不能 迭代 队列, 因为 其中 没有 元素 可用 于 迭代。 类 SynchronousQueue 经常 在 多个 线程 之间 传输 数据 时 使用。
DelayQueue
LinkedTransferQueue
作用和SynchronousQueue一样
Future&Callable
作用
在默认情况下, 线程Thread 对象不具有返回值的功能, 如果在需要取得返回值的情况下是极为不方便的, 但在 Java1. 5 的并发包中可以使用 Future 和 Callable 来使 线程具有返回值的功能。
与Runnable接口的区别
1) Callable 接口 的 call() 方法 可以 有 返回 值, 而 Runnable 接口 的 run() 方法 没有 返回 值。
2) Callable 接口 的 call() 方法 可以 声明 抛出 异常, 而 Runnable 接口 的 run() 方法 不可以 声明 抛出 异常。
执行 完 Callable 接口 中的 任务 后, 返回 值 是 通过 Future 接口 进行 获得 的。
用法
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(new MyThread());
System.out.println(future.get());
ExecutorService 接口 中的 方法 submit( Runnable,T result),第 2 个 参数 result 可以 作为 执行 结果 的 返回 值, 而 不需要 使用 get() 方法 来 进行 获得。
接口 Future 的 实现 类 是 FutureTask. java
Future.get()方法是阻塞的
isDone() 方法 无阻 塞 特性。
方法 cancel( boolean mayInterruptIfRunning) 的 参数 mayInterruptIfRunning 的 作用 是: 如果 线程 正在 运行 则是 否 中断 正在 运行 的 线程, 在 代码 中 需要 使用 if( Thread. currentThread(). isInterrupted()) 进行 配合。
任务 在 没有 运行 完成 之前 执行 了 cancel() 方法 返回 为 true, 代表 成功 发送 取消 的 命令。
方法 get( long timeout, TimeUnit unit) 的 作用 是在 指定 的 最大 时间 内 等待 获得 返回 值。
CompletebleFuture
RejectedExecutionHandler
ExecutorService的execute()与submit()区别
返回值
方法 execute() 没有 返回 值
而 submit() 方法 可以 有 返回 值。
异常
方法 execute() 在 默认 的 情况下 异常 直接 抛出, 不能 捕获, 但可以 通过 自定义 Thread- Factory 的 方式 进行 捕获
submit() 方法 在 默认 的 情况下, 可以 catch Execution- Exception 捕获 异常。
Executor
继承关系
Executor(接口)
ExecutorService(接口)
AbstractExecutorService(抽象类)
ThreadPoolExecutor(类)
ForkJoinPool(类)
ScheduledExcutorService(抽象类)
ScheduledThreadPoolExcutor
线程池
Executors工具类
常见的线程池
newFixedThreadPool
固定大小的线程池,有新任务时,根据当前线程池的线程数量确定后续步骤
创建参数
LinkedBlockingQueue(无界)
线程存活时间:永久存活
核心线程数:n(用户指定)
最大线程数:n(用户指定)
newCachedThreadPool
当有新任务时,直接新建线程
创建参数
SynchronousQueue
线程存活时间:60s
核心线程数:0
最大线程数:Interget.MAX_VALUE
newSingleThreadExecutor
创建单个线程的线程池。
创建参数
LinkedBlockingQueue(无界)
线程存活时间:永久存活
核心线程数:1
最大线程数:1
NewScheduledThreadPool:
创建一个定长线程池,支持定时及周期性任务执行。
创建参数
DelayedWorkQueue
一个按超时时间升序排序的队列
使用了优先级队列的无界阻塞队列,支持延时获取,所谓延时队列就是消费线程将会延时一段时间来消费元素。队列里的元素要实现Delay接口。
线程存活时间:永久存活
核心线程数:用户指定
最大线程数:Integer.MAX_VALUE
ThreadPoolExecutor
创建
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize: 池中所保存的线程数, 包括空闲线程, 也就是核心池的大小。
maximumPoolSize: 池中允许的最大线程数。
keepAliveTime: 当线程数量大于corePoolSize 值时, 在没有超过指定的时间内是不从线程池中将空闲线程删除的, 如果超过此时间单位,则删除
unit: keepAliveTime 参数的时间单位。
workQueue: 执行前用于保持任务的队列。 此队列仅保持由 execute 方法提交的Runnable 任务。
阻塞队列策略
同步移交
SynchronousQueue
不存储元素的阻塞队列,因此超出核心线程数的任务会创建新的线程来指执行。
无界队列
常用的为无界的LinkedBlockingQueue
当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM
DelayedWorkQueue
一个按超时时间升序排序的队列
使用了优先级队列的无界阻塞队列,支持延时获取,所谓延时队列就是消费线程将会延时一段时间来消费元素。队列里的元素要实现Delay接口。
有界队列
常见队列
ArrayBlockingQueue
遵循FIFO的队列
LinkedBlockingQueue
有节的LinkedBlockingQueue
PriorityBlockingQueue
优先级队列
可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量(因为任务数量少了)
threadFactory:线程工厂,创建线程
handler:阻止执行时要使用的处理程序 因为达到了线程边界和队列容量(线程池忙,任务队列满时拒绝策略)
拒绝策略
丢弃当前任务
丢弃最老的任务
直接抛出异常
抛回调用者的线程处理
自定义
其它方法
shutdown()
方法 shutdown() 的 作用 是 使 当前 未 执行 完 的 线程 继续 执行, 而 不再 添加 新的 任务 Task, 还有 shutdown() 方法 不会 阻塞, 调用 shutdown() 方法 后, 主 线程 main 就 马上 结束 了, 而 线程 池 会 继续 运行 直到 所有 任务 执行 完 才会 停止。 如果不 调用 shutdown() 方法, 那么 线程 池 会 一直 保持 下去, 以便 随时 执行 被 添加 的 新 Task 任务。
以上的文字一句话,已经接下的客人继续服务完,但是停止接客
当 线程 池 调用 shutdown() 方法 时, 线程 池 的 状态 则 立刻 变成 SHUTDOWN 状态, 此时 不 能再 往 线程 池 中 添加 任何 任务, 否则 将会 抛出 RejectedExecutionException 异常。 但是, 此时 线程 池 不会 立刻 退出, 直到 线程 池 中的 任务 都已 经处 理 完成, 才会 退出。
shutdownNow()
方法 shutdownNow() 的 作用 是 中断 所有 的 任务 Task, 并且 抛出 InterruptedException 异常, 前提 是在 Runnable 中 使用 if(if( Thread. currentThread(). isInterrupted()== true) 语句 来 判断 当前 线程 的 中断 状态, 而未 执行 的 线程 不再 执行, 也就是 从 执行 队列 中 清除。 如果 没有 if( Thread. currentThread(). isInterrupted()== true) 语句 及 抛出 异常 的 代码, 则 池 中正 在 运行 的 线程 直到 执行 完毕, 而未 执行 的 线程 不再 执行, 也从 执行 队列 中 清除。
以上文字一句话,接下的客,判断是否服务完,如果告知人家服务完,则全部清除,否则等待服务完已结客人,但不接待未接的客人
而 shutdownNow() 方法 是 使 线程 池 的 状态 立刻 变成 STOP 状态, 并 试图 停止 所有 正在 执行 的 线程( 如果 有 if 判断 则 人为 地 抛出 异常), 不再 处理 还在 池 队列 中 等待 的 任务, 当然, 它 会 返回 那些 未 执行 的 任务。
isShutdown()
方法 isShutdown() 的 作用 是 判断 线程 池 是否 已经 关闭。
只要 调用 了 shutdown() 方法, isShutdown() 方法 的 返回 值 就是 true。
线程池状态
running
自然是运行状态,指可以接受任务执行队列里的任务
shutdonw
SHUTDOWN 指调用了 shutdown() 方法,不再接受新任务了,但是阻塞队列里的任务得执行完毕。
stop
STOP 指调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。
tidying
所有任务都执行完毕,在调用 shutdown()/shutdownNow() 中都会尝试更新为这个状态。
terminated
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态
线程池的配置
IO密集型
尽量使用较小的线程池,一般为CPU核心数+1
Cpu密集型
可以使用稍大的线程池,一般为2*CPU核心数。
混合型
源码
Worker
Worker是线程池中的线程
继承AQS,实现runnable
既是一个可执行的任务,又可以达到锁的效果
初始state=-1
这样构造的原因主要是为了实现对中断的控制
1.worker未运行时
1.shutdown()线程池时,会对每个worker tryLock()上锁,tryAcquire是尝试通过CAS将state由0设置为1,因此会失败
2.shutdownNow()线程池时,不用tryLock()上锁,但调用worker.interruptIfStarted()终止worker时也要求state=0,因此也会失败
2.worker运行时
runWorker中,会对正在运行中的worker加锁,所以如果调用了shutdown()方法,中断也会失败,但是如果调用shutdownNow()方法,该方法会通过worker.interruptIfStarted来中断任务
addWorker
1.判断线程池状态,如果状态正常,进入下一步
2.比较当前线程池的线程数和最大线程数/核心线程数的大小(由参数core确定比较对象),如果没有超过的话,进入下一步,否则返回false
3.在线程池自带的成员变量ReentrantLock的加锁的情况下,向Workers的HashSet中添加新创建的worker实例,添加完成后解锁,并start该worker实例,worker.start()方法底层其实调用的就是runWorker()方法
基本概念:创建新线程并执行
runWorker
1.将state设置为0
2.在mainLock.lock的情况下,进行task.run
3.在finally块中释放锁mainLock.unlock
4.在使用getTask方法去阻塞队列中获取锁
ForkJoinPool
基础
实现方式
继承自Thread,重写run函数
实现runnable接口,实现run方法
实现Callable接口,实现call方法
三种实现方式的区别
实现Runnable接口可以避免Java单继承的局限;代码可以被多个线程共享,代码与数据是独立的;适合多个相同代码的线程处理同一资源的情况
继承Thread类和实现Runnable接口启动线程都是使用start方法,然后JVM将线程放到就绪队列中,如果有处理机可用,则调用run方法
实现callable接口,方法可以有返回值,并且可以抛出异常,需要FutureTask实现类的支持,用于接收执行结果
启动
调用start方法
线程的启动具有随机性
常用API
currentThread()
获得当前线程,如果在main中调用,当前线程是main而不是被start的线程
isAlive()
判断当前线程是否处于活动状态
线程 处于 正在 运行 或 准备 开始 运行 的 状态, 就 认为 线程 是“ 存活” 的。
sleep()
方法 sleep() 的 作用 是在 指定 的 毫秒 数 内 让 当前“ 正在 执行 的 线程” 休眠( 暂停 执行)。 这个“ 正在 执行 的 线程” 是指 this. currentThread() 返回 的 线程。
但是,不释放对象锁;需要抛出中断异常。
yield()
放弃当前的CPU资源,进入就绪状态;将它 让给 其他相同优先级的任务 去 占用 CPU 执行 时间。 和sleep相比,同样不释放锁,但不能控制具体交出CPU的时间,放弃当前的CPU资源,进入就绪状态,所以有可能 刚刚放弃, 马上又获得 CPU 时间 片。
wait
进入阻塞状态,释放锁,交出cpu执行权限;object方法
join()
当调用thread1.join()方法后,main线程(当前父线程)会进入等待,然后等待thread1执行完之后再继续执行。
join方法会让线程进入阻塞状态,(父线程)释放对一个对象持有的锁,并交出CPU执行权限
抛出中断异常
getId()
getId() 方法 的 作用 是 取得 线程 的 唯一 标识。
停止线程
使用 退出 标志, 使 线程 正常 退出, 也就是 当 run 方法 完成 后 线程 终止。
stop()
这个 方法 是 不安全 的( unsafe), 而且 是 已被 弃 用 作废 的( deprecated)
调用 stop() 方法 时会 抛出 java. lang. ThreadDeath 异常, 但在 通常 的 情况下, 此 异常 不需要 显 式 地 捕捉。
destroy()
废弃的方法
使用 interrupt() 方法 中断sleep、wait状态的线程。
interrupt()
单独调用interrupt方法可以使处于阻塞的线程抛出中断异常,中断处于阻塞状态的线程
调用 interrupt() 方法 仅仅是 在当 前 线程 中 打了 一个 停止 的 标记, 并不是 真的 停止 线程。
测试 当前 线程 是否 已经 中断。 线程 的 中断 状态 由该 方法 清除。 换句话说, 如果 连续 两次 调用 该 方法, 则 第二次 调用 将 返回 false( 在 第一次 调用 已 清除 了 其中 断 状态 之后, 且 第二次 调用 检验 完 中断 状态 前, 当前 线程 再次 中断 的 情况 除外)。
isInterrupt()
测试 线程 Thread 对象 是否 已经 是 中断 状态, 但不 清除 状态 标志。
通过interrupt方法和isInterrupted()方法来停止正在运行的线程,类似标志位的用法
捕获IO异常,中断I/O阻塞状态
抛出InterruptedException
if (this. interrupted()) { System. out. println(" 已经 是 停止 状态 了! 我要 退出 了!"); throw new InterruptedException(); }
使用 return 停止 线程
建议 使用“ 抛 异常” 的 方法 来 实现 线程 的 停止, 因为 在 catch 块 中 还可以 将 异常 向上抛, 使 线程 停止 的 事件 得以 传播。
暂停/恢复线程
suspend()
废弃的方法
缺点
此方法是独占的,如果拿到锁并且调用了线程的suspend()方法,那其他线程就再也拿不到锁,无法执行了
容易 出现 因为 线程 的 暂停 而 导致 数据 不 同步 的 情况。
resume()
线程间通信
等待/通知
wait()
方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断为止。在调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。在执行wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch语句进行捕捉异常。
wait()方法被执行后,锁立即释放
线程呈wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常。
wait(1000),等待1s,超过1s自动唤醒
当多个执行相同任务的线程,条件判断wait()的时候要用while而不能用if
当线程wait之后,又被唤醒的时候,是从wait后面开始执行,而不是又从头开始执行的,所以如果用if的话,被唤醒之后就不会在判断if中的条件,而是继续往下执行了
notify()
notify()也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程对其发出通知notify,并使它等待获取该对象的对象锁。
需要说明的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。
notify()方法执行后,不自动释放,必须执行完notify()方法所在的同步synchronized代码块后才释放锁。
notifyAll()唤醒所有线程
线程状态切换图
两个队列
就绪队列
存储将要获得锁的线程
被notify()并且拿到锁的线程
阻塞队列
存储被阻塞的线程
调用wait()的线程
不同线程间直接传送数据
字节流
PipedInputStream
PipedOutputStream
字符流
PipedReader
PipedWriter
join
作用:创建子线程的主线程,必须等待子线程执行完毕,或销毁后后,才能够提前执行完成
thread.join()
join(long)中的参数是设定等待的时间
方法join(long)的功能在内部是使用wait(long)方法来实现的,所以join(long)方法具有释放锁的特点。
ThreadLocal
作用:解决的就是每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有数据。
基本用法
set(value)
首先获取当前thread的ThreadLocalMap
如果map已经初始化,则将kv存入map中
否则初始化map(此时构造函数已经将kv存入)
get()
remove()
initialValue()
解决threadLocal初始化get()为null的问题
继承自ThreadLocal类,复写initialValue()方法
底层实现
ThreadLocalMap是ThreadLocal的内部类
ThreadLocalMap
用Entry进行存储,Entry的key的类型为ThreadLocal
每个Thread维护了一个ThreadLocalMap的成员变量,这是实现ThreadLocal的核心
hash冲突解决方法:线性探测法
set():向ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
get(): 从ThreadLocalMap获取值,key是ThreadLocal对象
内存泄漏
ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项。如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
解决方法
调用threadLocal.remove()方法来清理key为null的元素
根本原因
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用
为什么key要用弱引用
引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
扩展
强软弱虚(strong soft week )
StrongRefference强引用
正常new,不会回收
SoftRefference<T>()软引用
内存不够就会回收:用作缓存
WeekRefference<T>()弱引用
遇到gc()就会回收
PhantomRefference<T>(,Queue)虚引用
指向堆外内存,jvm内获取不到
应用场景
单个线程
线程上下文信息存储
数据库连接
session管理
InheritableThreadLocal
以在子线程中取得父线程继承下来的值。
线程的优先级
setPriority()
线程 的 优先级 分为 1~ 10 这 10 个 等级, 如果 小于 1 或 大于 10, 则 JDK 抛出 异常 throw new IllegalArgumentException()。
JDK 中 使用 3 个 常量 来 预置 定义 优先级 的 值,
public final static int MIN_ PRIORITY = 1;
public final static int NORM_ PRIORITY = 5;
public final static int MAX_ PRIORITY = 10;
特性
继承 特性
规律性:高优先级的线程总是先执行完
随机性:高优先级的线程不一定每一次都先执行完
线程的分类
用户线程
守护线程
守护线程是一种特殊的线程,它的特性有“ 陪伴”的含义,当创建他的进程中不存在其他非守护线程了,则守护线程自动销毁。
典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。
当JVM中没有用户线程了,就算守护线程没有执行完毕也会被关闭
但是在主线程中调用守护线程.join(),那就守护不了了
对象及变量的并发访问
synchronized
synchronized同步方法
其方法内的变量为线程安全
实例变量非线程安全
两个线程访问同一个对象中的同步方法时一定是线程安全的
java中的锁是对象锁:如果拿到锁,则其他线程是不能调用对象中的所有synchronized方法的
重入锁
也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。
这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
出现异常,锁自动释放
同步不具有继承性,需要重写synchronized关键字,synchronized关键字不是方法的一部分
synchronized同步语句块
同步方法的弊端,方法中不需要同步的代码也做了同步,增加了方法的执行时间,效率低,不够灵活
并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
不在synchronized块中就是异步执行,在synchronized块中就是同步执行。
当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。
采用“序列化访问临界资源”的方案,调用按顺序执行,也就是同步的,阻塞的。
synchronized同步方法
对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
非synchronized方法或者静态方法可以被调用
同一时间只有一个线程可以执行 synchronized同步方法中的代码。
synchronized(this)同步代码块
对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
同一时间只有一个线程可以执行synchronized(this)同步代码块中的代码。
锁非this对象
如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可大大提高运行效率。
使用“synchronized(非this对象x)同步代码块”格式进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了,就会交叉运行。
同步代码块放在非同步synchronized方法中进行声明,并不能保证调用方法的线程的执行同步/顺序性,也就是线程调用方法的顺序是无序的,虽然在同步块中执行的顺序是同步的,这样极易出现“脏读”问题。
锁得几种类型
🔒对象锁
添加在方法上和代码块上的锁都是对象锁
Class锁
synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类进行持锁
Class锁可以对类的所有对象实例起作用
在静态方法上加Synchronized 等同于 Synchoronized(this.getClass())
死锁
JAVA_HOME下执行jps列出java进程
执行jstack 进程id
出现异常,JVM自动释放锁,不会由于异常导致死锁现象
Lock的使用
ReentrantLock
调用ReentrantLock对象的lock()方法获取锁,调用unlock()方法释放锁。
基本用法
Lock lock = new ReentrantLock();
lock.lock();
//do something else.
lock.unlock();应将锁的释放卸载finally中
Condition
Condition类是在JDK5中出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。
Condition condition = lock.newCondition();
condition.wait()
Object类中的wait()方法相当于Condition类中的await()方法。
Object类中的wait(longtimeout)方法相当于Condition类中的await(longtime,TimeUnitunit)方法。
condition.signal()
Object类中的notify()方法相当于Condition类中的signal()方法。
Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。
公平锁和非公平锁
公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。
非公平锁就是一种获取锁的抢占机制,是随机获得锁的,先到不一定先得到锁,这个方式可能造成某些线程一直拿不到锁
new ReentrantLock(isFair)
ReentrantReadWriteLock
共享锁
关系
读操作相关的锁
读锁之间不互斥
读锁与写锁互斥
使用
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
// do something else.
lock.readLock().unlock();
排它锁
关系
写操作相关的锁
写锁与写锁互斥
使用
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock();
// do something else.
lock.writeLock().unlock();
相关API
lock.getHoldCount()
查询当前线程报错此锁定的个数,也就是调用lock()的次数
lock.getQueueLength()
返回正等待获取此锁定的线程估计数,比如有5个线程,1个线程首先执行await()方法,那么在调用getQueueLength()方法后返回值是4,说明有4个线程同时在等待lock的释放。
lock.getWaitQueueLength(condition)
返回等待与此锁定相关的给定条件Condition的线程估计数,比如有5个线程,每个线程都执行了同一个condition对象的await()方法,则调用getWaitQueueLength(Conditioncondition)方法时返回的int值是5。
lock.hasQueuedThread(thread)
查询指定的线程是否正在等待获取此锁定
lock.hasQueuedThreads()
查询是否有线程正在等待此锁定
lock.hasWaiters(condition)
是查询是否有线程正在等待与此锁定有关的condition条件
lock.isFair()
lock.isHeldByCurrentThread()
查询当前线程是否保持此锁定
lock.isLocked()
查询此锁定是否由任意线程保持
lock.lockInterruptibly()
如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常
lock.tryLock()
仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定
lock.tryLock(long timeout, TimeUnit unit)
方法booleantryLock(longtimeout,TimeUnitunit)的作用是,如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定
volatile
关键字volatile的主要作用是使变量在多个线程间可见。
volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。
类的成员变量存在于公共堆栈及线程的私有堆栈中。在JVM被设置为-server模式时为了线程运行的效率,线程一直在私有堆栈中取值
线程的私有堆栈
读取公共内存
关键字volatile虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。
变量在内存中的工作过程
使用原子类进行i++操作
AtomicInteger
synchronized代码块有volatile同步的功能
定时器
Timer
TimerTask是以队列的方式一个一个被顺序执行的,所以执行的时间有可能和预期的时间不一致,因为前面的任务有可能消耗的时间较长,则后面的任务运行的时间也会被延迟。
如果执行任务的时间早于当前时间,则立即执行task任务。
cancel
作用是将任务队列中的全部任务清空。
Timer类中的cancel()方法有时并不一定会停止执行计划任务,而是正常执行。
有时并没有争抢到queue锁,所以TimerTask类中的任务继续正常执行。
TimerTask
几种用法
schedule( TimerTask task, Date time)
在指定的日期执行一次某一任务。
schedule(TimerTask task,Date firstTime,long period)
是在指定的日期之后,按指定的间隔周期性地无限循环地执行某一任务。
schedule( TimerTask task, long delay)
以当前的时间为参考时间,在此时间基础上延迟指定的毫秒数后执行一次TimerTask任务。
schedule( TimerTask task, long delay, long period)
当前的时间为参考时间,在此时间基础上延迟指定的毫秒数,再以某一间隔时间无限次数地执行某一任务。
scheduleAtFixedRate( TimerTask task, Date firstTime, long period)
cancel
将自身从任务队列中清除。其他任务不受影响。
凡是使用方法中带有period参数的,都是无限循环执行TimerTask中的任务。
单例模式&多线程
饱汉模式
不存在线程安全问题
饿汉模式
存在线程安全问题
解决:加锁,加同步块
但是效率低
应该只针对需要同步的代码部分加锁
所以引入DLC双重锁定机制
线程死锁
死锁产生的四个必要条件
互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请的资源。
解决方法
解决任一条件
锁分类
乐观锁,悲观锁
乐观锁
CAS算法
悲观锁
synchronized
独享锁,共享锁
公平锁,非公平锁
可重入锁
线程得到一个对象锁后再次请求该对象锁,是允许的
实现方法
为每一个锁关联一个获取计数器和一个所有者线程,在获取锁的时候判断owner,如果是相同的owner,则count加1
分段锁
通过分段锁的形式来细化锁的粒度,从而实现高效的并发操作,例如concurrentHashMap
锁优化
偏向锁
CAS操作:将线程ID保存在对象的Mark Word中
如果成功,则说明该线程已经获取了对象的偏向锁
有其他线程获取对象锁时失效
对象锁定时膨胀为轻量级锁
对象未锁定时恢复到未锁定状态
经验依据:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得
轻量级锁
CAS操作:将对象的Mark Word更新为指向Lock Record的指针
如果成功,则该线程已经获取了对象的轻量级锁
如果失败,检查对象的Mark Word是否指向当前线程
如果指向当前线程,则进入同步代码块
如果没有,则膨胀为重量级锁
如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
经验依据:对绝大部分的锁,在整个同步周期内都不存在竞争
重量级锁
自旋锁
尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
锁粗化
如果一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
锁消除
对于一些代码上要求同步,但实际上并不需要同步的锁进行消除
0 条评论
下一页