本笔记来自 计算机程序的思维逻辑 系列文章
线程
创建线程的方式
- 继承
Thread
- 实现
Runnable
接口
属性和方法
long tid
线程ID,递增整数String name
线程名,默认以 Thread- + 线程编号 构成int priority
优先级,范围是 1 到 10 ,默认是 5int threadStatus
状态,枚举类型:NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
-
boolean daemon
是否守护线程当程序中只存在
daemon
线程时,程序就会退出程序运行时,除了创建
main
线程,至少还会创建一个负责垃圾回收的线程,它就是daemon
线程,当main
线程结束时,垃圾回收线程也会退出 boolean isAlive()
判断线程是否存活void yield()
表示当前线程不着急使用CPUvoid sleep(long millis)
让当前线程睡眠一段时间void join(long millis)
等待当前线程,0 表示无限等待
特点
共享内存
每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈,但线程之间可以共享内存,它们可以访问和操作相同的对象
竞态条件
当多个线程访问和操作同一个对象时,最终结果与执行时序有关,可能正确也可能不正确
解决方法:使用synchronized
关键字,使用显式锁,使用原子变量
内存可见性
多个线程可以共享访问和操作同一个变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,可能永远看不到
在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中取,当修改一个变量时,也可能是先写到缓存中,而稍后才会同步更新到内存中
优点
- 充分利用多CPU的计算能力,单线程只能利用一个CPU
- 充分利用硬件资源,多个独立的网络请求,完全可以使用多个线程同时请求
- 在用户界面应用程序中,保持程序的响应性,界面和后台任务通常是不同的线程
- 简化建模和IO处理
成本
- 创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建需要时间
- 当有大量可运行线程时,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行。切换出去时,系统需要保存线程当前上下文状态到内存;切换回来时,需要恢复。这种切换会使CPU的缓存失效,而且耗时
- 创建超过CPU数量的线程是不必要的
synchronized
可以用于修饰类的实例方法、静态方法和代码块
实例方法
- 保护同一对象的方法调用,实际保护的是当前实例对象
- 对象有一个锁和一个等待队列,锁只能被一个线程持有,当前线程不能获得锁时,会加入等待队列
- 保护的是对象而不是代码,只要访问的是同一个对象的
synchronized
方法,即使是不同的方法,也会同步顺序访问
静态方法
- 保护的是类对象
- 不同的两个线程,可以同时一个执行
synchronized
静态方法,另一个执行synchronized
实例方法
代码块
- 在方法中使用
synchronized
,传入一个对象,即保护对象 - 实例方法用
this
,静态方法则用Class
特点
可重入性
- 通过记录锁的持有线程和持有数量来实现
- 同个线程,在获得锁后,调用其它需要同样锁的代码时,可以直接调用;即一个线程调用的一个
synchronized
实例方法内可以直接调用其它synchronized
实例方法
内存可见性
- 在释放锁时,所有写入都会写回内存,获得锁时,都会从内存读取最新数据。成本有点高
- 给变量加修饰符
volatile
,保证读写到内存最新值
死锁
- a持有锁A,等待锁B,b持有锁B,等待锁A,陷入互相等待
- 应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同顺序去申请锁
同步容器
通过给所有容器方法加上synchronized
来实现安全。如:SynchronizedCollection
但以下情况不是安全的
- 复合操作
- 伪同步
- 迭代
并发容器
同步容器性能比较低,Java有很多专门为并发设计的容器类。如:CopyOnWriteArrayList
ConcurrentHashMap
线程的基本协作机制
wait 等待
把当前线程放到条件等待队列并阻塞,等待唤醒
过程
- 把当前线程放入条件等待队列,释放对象锁,阻塞等待,线程状态变为 WAITING 或 TIMED_WAITING
- 等待时间到或被其它线程调用
notify
/notifyAll
从条件等待队列中移除,这时,要重新竞争对象锁- 如果能够获得锁,线程状态变为 RUNNABLE ,并从
wait
调用中返回 - 否则,该线程加入对象锁等待队列,线程状态变为 BLOCKED ,只有在获得锁后才会从
wait
调用中返回
- 如果能够获得锁,线程状态变为 RUNNABLE ,并从
线程从wait
调用中返回后,不代表其等待的条件就一定成立了,它需要重新检查其等待的条件。
一般调用模式
synchronized (obj) {
while (条件不成立) {
obj.wait();
}
// ... 执行条件满足后的操作
}
notify 唤醒
从条件等待队列中移除一个线程并将之唤醒
notifyAll 唤醒
移除条件等待队列中所有线程并全部唤醒
机制
- 每个对象都有一把锁和用于锁的等待队列,还有一个条件等待队列,用于线程间的协作
- 一个对象调用
wait
方法就会当前线程放到条件队列上并阻塞,表示当前线程执行不下去了,需要等待一个条件,这个条件它本身无法改变,需要其它线程改变 - 当其它线程改变了条件后,调用该对象的
notify
或notifyAll
方法,将其唤醒
注意
-
wait
和notify
方法只能在synchronized
代码块内被调用,如果调用时,当前线程没有持有对象锁,会抛异常IllegalMonitorStateException
- 虽然是在
synchronized
方法内,但调用wait
时,线程会释放锁 -
notify
方法不会释放锁
协作的核心
共享的条件变量
场景
- 同时开始 共享同一个条件变量
-
等待结束
CountDownLatch
-
异步结果
Executor
Future
-
集合点
CyclicBarrier
线程的中断
取消/关闭的机制
在Java中,停止一个线程的主要机制是中断,中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何及何时退出
每个线程都有一个标志位,表示该线程是否被中断了
-
void stop()
已过时 -
boolean isInterrupted()
返回对应线程的中断标志位 -
void interrupt()
中断对应的线程 -
static boolean interrupted()
返回当前线程的中断标志位;同时清空中断标志位
线程对中断的反应
interrupt
对线程的影响与线程的状态和在进行的IO操作有关
RUNNABLE
线程在运行或具备运行条件只是在等待操作系统调度
- 如果线程在运行中,且没有执行IO操作,
interrupt
只是会设置线程的中断标志位,没有任何其它作用;线程应该在运行过程中合适的位置检查中断标志位
WAITING / TIMED_WAITING
线程在等待某个条件或超时
- 线程执行
join()
或wait()
会进入 WAITING 状态 - 线程执行
wait(long timeout)
sleep(long millis)
或join(long millis)
会进入 TIMED_WAITING 状态 - 在这些状态时,调用
interrupt
会使线程抛异常InterruptedException
,抛异常后,中断标志位被清空 - 捕获到
InterruptedException
,通常希望结束该线程,有两种处理方式- 向上传递该异常,使得该方法也变成了一个可中断的方法,需要调用者进行处理
- 不能向上传递时,捕获异常,进行合适的清理操作,清理后,一般调用
interrupt
方法设置中断标志位,使其它代码知道它发生了中断
BLOCKED
线程在等待锁,试图进入同步块
- 如果线程在等待锁,调用
interrupt
只会设置线程的中断标志位,线程依然处于 BLOCKED 状态
NEW / TERMINATE
线程还没启动或已结束
- 在这些状态时,调用
interrupt
对它没有任何效果,中断标志位也不会被设置
IO操作
- 如果IO通道是可中断的,即实现了
InterruptibleChannel
接口,则IO操作关闭,线程的中断标志位会被设置,同时线程会收到异常ClosedByInterruptException
- 如果线程阻塞于
Selector
调用,则线程的中断标志位会被设置,同时阻塞的调用会立即返回
正确取消/关闭线程
-
interrupt
方法不会真正中断线程,只是一种协作机制 - 以线程提供服务的程序模块,应该封装取消/关闭操作,提供单独的取消/关闭方法给调用者,而不是直接调用
interrupt
方法
原子变量和CAS
包含一些以原子方式实现组合操作的方法
基本原子变量类型
- AtomicInteger
- AtomicBoolean
- AtomicLong
- AtomicReference,AtomicMarkableReference,AtomicStampedReference
数组类型
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
更新类
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
内部实现
依赖compareAndSet
方法,简称 CAS
CAS 是Java并发包的基础,基于它可以实现高效、乐观、非阻塞式的数据结构和算法,也是并发包中锁、同步工具和各种容器的基础
对比
-
synchronized
是悲观的,它假定更新很可能冲突,所以先获得锁,得到锁后才更新;原子变量
是乐观的,它假定冲突比较少,但使用CAS更新,也就是进行冲突检测,如果冲突,就继续尝试 -
synchronized
是阻塞式算法,得不到锁时,进入锁等待队列,等待其它线程唤醒,有上下文切换开销;而原子变量
是非阻塞式的,更新冲突时,就重试,不会阻塞,不会有上下文切换开销
显式锁
支持以非阻塞方式获取锁,可以响应中断,可以限时
接口 Lock
-
void lock()
获取锁,会阻塞直到成功 -
void unlock()
释放锁 -
void lockInterruptibly()
可以响应中断,被其它线程中断时,抛出InterruptedException
异常 -
boolean tryLock()
只是尝试获取锁,立即返回,不阻塞;如果获取成功,返回 true ,否则返回 false -
boolean tryLock(long time, TimeUnit unit)
先尝试获取锁,如果成功则立即返回 true ,否则阻塞等待,等待最长时间为指定的参数,在等待的同时响应中断,如果发生中断,抛出InterruptedException
异常,如果在等待时间内获得锁,返回 true ,否则返回 false -
Condition newCondition()
新建一个条件,一个Lock
可以关联多个条件
可重入锁 ReentrantLock
基本用法
该类的lock
和unlock
方法实现了与synchronized
一样的语义
- 可重入,即一个线程在持有一个锁的前提下,可以继续获得该锁
- 可以解决竞态条件问题
- 可以保证内存可见性
带参数boolean fair
的构造方法
- 参数 fair 表示是否保证公平,不指定的情况下,默认为 false ,表示不保证公平
- 公平指等待最长时间的线程优先获得锁;保证公平会影响性能,一般不需要
-
synchronized
锁也是不保证公平的
使用显式锁,一定要记得调用unlock
,一般而言,应该将lock
之后的代码包装到try
语句内,在finally
语句内释放锁
使用tryLock避免死锁
在持有一个锁,获取另一个锁,获取不到的时候,可以释放已持有的锁,给其它线程机会获取锁,然后再重试获取所有锁
获取锁信息
用于监控和调试
-
boolean isLocked()
是否被持有,不一定是当前线程持有 -
int getHoldCount()
锁被当前线程持有的数量;0 表示不被当前线程持有 -
boolean isHeldByCurrentThread()
是否被当前线程持有 -
boolean isFair()
锁等待策略是否公平 -
boolean hasQueuedThreads()
是否有线程在等待该锁 -
boolean hasQueuedThread(Thread thread)
指定的线程是否在等待该锁 -
int getQueueLength()
在等待该锁的线程个数
实现原理
依赖 CAS 和 LockSupport
类
LockSupport
-
void park()
使当前线程放弃CPU,进入等待状态,操作系统不再对它进行调度,直到有其它线程对它调用了unpark
-
void parkNanos(long nanos)
指定等待的最长时间,相对时间 -
void parkUntil(long deadline)
指定最长等到什么时候,绝对时间 -
void unpark(Thread thread)
使线程恢复可运行状态
显式条件
显式条件与wait
/notify
相对应
wait
/notify
与synchronized
配合使用;显式条件与显式锁配合使用
Condition
通过显式锁创建 Condition newCondition()
void await()
对应于wait()
void signal()
对应于notify()
void signalAll()
对应于notifyAll()
-
boolean await(long time, TimeUnit unit)
指定等待的时间,相对时间如果等待超时,返回 false ,否则为 true
-
long awaitNanos(long nanosTimeout)
指定等待的时间,相对时间返回值是 nanosTimeout 减去实际等待的时间
-
boolean awaitUntil(Date deadline)
指定最长等到什么时候,绝对时间如果等待超时,返回 false ,否则返回 true
-
void awaitUninterruptibly()
不响应中断的等待如果等待过程发生了中断,中断标志位会被设置
机制
- 和
wait
方法一样,调用await
方法前需要先获取锁,如果没有锁,会抛IllegalMonitorStateException
异常 -
await
在进入等待队列后,会释放锁,释放CPU - 当其它线程将它唤醒后,或等待超时后,或发生中断异常后,它都需要重新获取锁,获取锁后,才会从
await
方法中退出 -
await
返回后,不代表其等待的条件就一定满足了,通常要将await
的调用放到一个循环内,只有条件满足后才退出
并发容器
CopyOnWriteArrayList
区别
- 线程安全,可以被多个线程并发访问
- 它的迭代器不支持修改操作,但也不会抛出
ConcurrentModificationException
异常 - 它以原子方式支持一些复合操作
原理
写时拷贝
- 内部是一个数组,但这个数组是以原子方式被整体更新的
- 每次修改操作,都会新建一个数组,复制原数组的内容到新数组,在新数组上进行需要的修改,然后以原子方式设置内部数据的引用
保证线程安全的思路
- 使用锁
- 循环CAS
- 写时拷贝
CopyOnWriteArraySet
基于 CopyOnWriteArrayList
实现
ConcurrentHashMap
区别
- 并发安全
- 直接支持一些原子复合操作
- 支持高并发、读操作完全并行、写操作支持一定程度的并行
- 迭代不用加锁,不会抛出
ConcurrentModificationException
异常 - 弱一致性
原子复合操作
实现了ConcurrentMap
接口
-
V putIfAbsent(K key, V value)
条件更新如果 key 不存在,则设置 key 为 value ,返回之前的值
如果 key 存在,返回对应的值
-
boolean remove(Object key, Object value)
条件删除如果 key 存在且对应值为 value ,则删除并返回 true ,否则返回 false
-
boolean replace(K key, V oldValue, V newValue)
条件替换如果 key 存在且对应值为 oldValue ,则替换为 newValue 并返回 true ,否则返回 false
-
V replace(K key, V value)
条件替换如果 key 存在,则替换值为 value 并返回之前的值,否则返回 null
原理
- 分段锁 将数据分为多个段,而每个段有一个独立的锁;每个段相当于一个独立的哈希表,分段的依据也是哈希值,无论是保存键值对还是根据键查找,都先根据键的哈希值映射到段,再在段对应的哈希表上进行操作
- 读不需要锁
弱一致性
迭代器创建后,按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反应出来,如果变化发生在未遍历的部分,迭代器就会发现并反映出来
ConcurrentSkipListMap
基于 SkipList
跳跃表 实现
特点
- 没有使用锁,所有操作都是无阻塞的,所有操作都可以并行,多个线程可以同时写
- 弱一致性,有些操作不是原子的
- 实现了
ConcurrentMap
接口,直接支持一些原子复合操作 - 实现了
SortedMap
和NavigableMap
接口,可排序,默认按键有序,可传递比较器自定义排序
跳表
基于 链表 ,在链表的基础上加了多层索引结构
高层的索引节点一定同时是低层的索引节点;高层的索引节点少,低层的多
每个索引节点,有两个指针,一个向右,指向下一个同层的索引节点,另一个向下,指向下一层的索引节点或基本链表节点
ConcurrentSkipListSet
基于 ConcurrentSkipListMap
实现
各种队列
无锁非阻塞并发队列
都是基于链表实现,没有限制大小,无界;适用于多个线程并发使用一个队列的场合
- ConcurrentLinkedQueue
- ConcurrentLinkedDeque
普通阻塞队列
都实现了BlockingQueue
接口,内部使用显式锁ReentrantLock
和显式条件Condition
- ArrayBlockingQueue 基于循环数组实现,有界,创建时指定大小,运行过程中不会改变
- LinkedBlockingQueue LinkedBlockingDeque 基于链表实现,默认无界
优先级阻塞队列
PriorityBlockingQueue 按优先级出队,优先级高的先出,无界
延时阻塞队列
DelayQueue
- 特殊的优先级队列,无界
- 要求每个元素都实现
Delayed
接口 - 按元素的延时时间出队,只有当元素的延时过期之后才能从队列中被拿走
其它阻塞队列
- SynchronousQueue 入队操作要等待另一个线程的出队操作,反之亦然
- LinkedTransferQueue 入队操作可以等待出队操作后再返回
异步任务执行服务
执行服务
线程Thread
既表示要执行的任务,又表示执行的机制
执行服务
将 任务的提交 和 任务的执行 相分离,封装了任务执行的细节;对任务的提交者而言,它可以关注于任务本身,如提交任务、获取结果、取消任务;而不需要关注任务执行的细节,如线程创建、任务调度、线程关闭
基本接口
Runnable
表示要执行的异步任务,没有返回结果,不会抛异常
Callable
同样指要执行的异步任务,有返回结果,会抛异常
Executor
表示执行服务,执行一个Runnable
,没有返回结果
ExecutorService
扩展了Executor
,定义了更多服务
submit
方法都表示提交任务,返回后,只是表示任务已提交,不代表已执行-
void shutdown()
关闭不再接收新任务,但已提交的任务会继续执行,即使任务还未开始执行
-
List<Runnable> shutdownNow()
关闭不再接收新任务,已提交但尚未执行的任务会被终止,并尝试中断正在执行的任务
返回已提交但尚未执行的任务列表
boolean isShutdown()
是否调用了shutdown
或shutdownNow
-
boolean isTerminated()
是否所有任务都已结束只有调用过
shutdown
或shutdownNow
并且所有任务都已结束才返回 true ,否则返回 false -
boolean awaitTermination(long timeout, TimeUnit unit)
限定等待时间,等待所有任务结束当所有任务都结束,返回 true
等待超时,返回 false
等待过程发生中断,会抛
InterruptedException
异常 -
invokeAll
等待所有任务完成可以指定等待时间,如果超时后有任务还没完成,就会被取消
-
invokeAny
只要有一个任务完成,则返回该任务的结果,其它任务会被取消可以指定等待时间,如果超时没有任务完成,会抛
TimeoutException
异常如果所有任务都发生异常,则抛
ExecutionException
异常
Future
表示异步任务的执行结果
-
boolean cancel(boolean mayInterruptIfRunning)
取消异步任务如果任务已完成、或已经取消、或由于某种原因不能取消,返回 false ,否则返回 true
如果任务还未开始,则不再运行
参数
mayInterruptIfRunning
表示任务正在执行,是否调用interrupt
方法中断线程 -
boolean isCancelled()
任务是否被取消只要
cancel
方法返回 true ,随后此方法都会返回 true -
boolean isDone()
任务是否结束任务正常结束、任务抛异常、任务被取消,都视为结束
-
V get()
返回异步任务最终的结果如果任务还未执行完成,会阻塞等待
V get(long timeout, TimeUnit unit)
同上,限定了阻塞等待的时间,如果超时任务还未结束,会抛TimeoutException
异常
任务结果
- 正常完成
- 异常 将原异常包装为
ExecutionException
重新抛出 - 取消 抛出
CancellationException
异常
线程池
概念
主要由 任务队列 和 工作者线程 组成
工作者线程主体是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务
优点
- 可以重用线程,避免线程创建的开销
- 在任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成
构造方法参数
corePoolSize
核心线程个数
有新任务时,如果当前线程数小于核心线程个数,就会创建一个新线程来执行该任务,即使其它线程是空闲的,也会创建新线程
如果线程个数大于等于核心线程个数,就不会立即创建新线程,会先尝试排队,如果队列满了或其它原因不能立即入队,就检查线程个数是否达到最大线程个数,如果没有,就继续创建线程,直到线程数达到最大线程个数
maximumPoolSize
最大线程个数
不管创建多少任务,都不会创建比这个值大的线程个数
keepAliveTime unit
空闲线程存活时间
目的是为了释放多余的线程资源
表示当线程池中的线程个数大于核心线程个数时,额外空闲线程的存活时间
workQueue
队列,要求是阻塞队列BlockingQueue
如果是无界队列,线程个数最多达到核心线程个数,到达后,新任务总会排队
handler
任务拒绝策略
如果队列有界,且最大线程个数有限,当队列满,线程个数也达到最大线程个数时,触发线程池的任务拒绝策略
四种处理方式
-
AbortPolicy
默认处理,抛出RejectedExecutionException
异常 -
DiscardPolicy
静默处理,忽略新任务,不抛异常,也不执行 -
DiscardOldestPolicy
将等待时间最长的任务扔掉,自己排队 -
CallerRunsPolicy
在任务提交者线程中执行任务,而不是交给线程池中的线程执行
threadFactory
线程工厂
根据Runnable
创建一个Thread
Executors
工厂类,方便创建一些预配置的线程池
newSingleThreadExecutor
只使用一个线程,使用无界队列,线程创建后不会超时终止,该线程顺序执行所有任务
适用于需要确保所有任务被顺序执行的场合
newFixedThreadPool
使用固定个数的线程,使用无界队列,线程创建后不会超时终止
如果排队任务过多,可能会消耗非常大的内存
newCachedThreadPool
当有新任务时,如果正好有空闲线程,则接受任务,否则总是创建一个新线程,总线程个数不受限制
对任一空闲线程,如果60秒内没有新任务,就终止
线程池的死锁
任务之间有依赖,可能会出现死锁
任务A在执行过程中,提交了任务B,需等待任务B结束,如果任务A提交给一个单线程线程池,或者任务B由于线程占满需要排队等待,那么就会出现死锁,A在等待B的结果,而B在队列中等待调度
CompletionService
场景
主线程提交多个异步任务,有任务完成就处理结果,并且按任务完成顺序逐个处理
方法
-
Future<V> take()
获取下一个完成任务的结果,阻塞等待 -
Future<V> poll()
获取下一个完成任务的结果,立即返回;如果没有已经完成的任务,返回 null -
Future<V> poll(long timeout, TimeUnit unit)
获取下一个完成任务的结果,限定等待时间
实现原理
- 依赖
Executor
完成实际的任务提交,自己负责结果的排队和处理 - 内部维护一个队列,用来保存任务结果
实现类
ExecutorCompletionService
定时任务
Timer 和 TimerTask
TimerTask
表示定时任务,实现Runnable
接口
Timer
负责定时任务的调度和执行
void schedule(TimerTask task, long delay)
当前时间延时 delay 毫秒后执行任务void schedule(TimerTask task, Date time)
再指定绝对时间 time 执行任务-
void schedule(TimerTask task, long delay, long period)
固定延时重复执行第一次执行时间为当前时间加上 delay ,后一次的计划执行时间为前一次 实际 执行时间加上 period
-
void schedule(TimerTask task, Date firstTime, long period)
固定延时重复执行第一次执行时间为 firstTime ,后一次的计划执行时间为前一次 实际 执行时间加上 period
如果 firstTime 是一个过去的时间,任务会立即执行
-
void scheduleAtFixedRate(TimerTask task, long delay, long period)
固定频率执行第一次执行时间为当前时间加上 delay ,后一次的计划执行时间为前一次 计划 执行时间加上 period
-
void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
固定频率执行第一次执行时间为 firstTime ,后一次的计划执行时间为前一次 计划 执行时间加上 period
如果 firstTime 是一个过去的时间,任务会立即执行
如果 firstTime 加上 period 还是一个过去时间,会连续运行多次,直到时间超过当前时间
基本原理
Timer
内部主要由 TaskQueue 和 TimerThread 两部分组成
TaskQueue 是一个基于堆实现的优先级队列,按照下次执行的时间排优先级
TimerThread 负责执行所有的定时任务,一个Timer
对象只有一个TimerThread
TimerThread 主体是一个循环,从队列中拿任务,如果队列中有任务且计划执行时间小于等于当前时间,就执行它;如果队列中没有任务或第一个任务延时还没到,就睡眠;如果睡眠过程中添加了新任务且新任务是第一个任务,该线程会被唤醒,重新进行检查
执行任务之前,该线程判断任务是否为周期任务,如果是,就设置下次执行的时间并添加到优先级队列中;对于固定延时的任务,下次执行时间为当前时间加上 period ;对于固定频率的任务,下次执行时间为上次计划执行时间加上 period
注意的是,下次任务的计划是在执行当前任务之前就做出的。
对于固定延时任务,延时相对的是任务执行前的当前时间,而不是任务执行后的
注意
-
死循环
一个
Timer
对象只有一个线程,意味着,定时任务不能耗时太长,更不能是无限循环 -
异常处理
在执行任何一个任务的
run
方法时,一旦抛出异常,TimerThread
就会退出,从而所有定时任务都会被取消为了各任务互不干扰,在
run
方法内捕获所有异常
ScheduledExecutorService
特点
- 基于线程池实现,可以有多个线程
- 对于周期任务,在任务执行后再设置下次执行的时间
- 任务执行线程会捕获任务执行过程中的所有异常,一个定时任务的异常不会影响其它定时任务,发生异常的任务会被取消
方法
-
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
延时 delay 后单次执行
-
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
延时 delay 后单次执行
-
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
固定频率,重复执行
第一次执行时间为 initialDelay 后,第二次为 initialDelay + period ,依此类推
-
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
固定延时,重复执行
对于固定延时任务,延时相对的是任务执行后的当前时间
并发同步协作工具
ReentrantReadWriteLock
可重入读写锁
特点
一个读锁,一个写锁;读操作使用读锁,写操作使用写锁
只有 读 - 读 操作可以并行,读 - 写 和 写 - 写 都不可以并行
只有一个线程可以进行 写 操作,在获取写锁时,只有没有任何线程持有任何锁才可以获取到;在持有写锁时,其它任何线程都获取不到任何锁
在没有其它线程持有写锁的情况下,多个线程可以获取和持有读锁
Semaphore
信号量
特点
限制对资源的并发访问数
一般锁只能由持有锁的线程释放,而Semaphore
表示的只是一个许可数,任意线程都可以调用其 release 方法
方法
void acquire()
获取许可,阻塞等待void acquireUninterruptibly()
获取许可,阻塞等待,不响应中断void acquire(int permits)
批量获取多个许可void acquireUninterruptibly(int permits)
批量获取多个许可-
boolean tryAcquire()
尝试获取许可,立即返回当前有可用许可,返回 true ,否则返回 false
boolean tryAcquire(long timeout, TimeUnit unit)
尝试获取许可,限定等待时间void release()
释放许可
CountDownLatch
倒计时门栓
特点
一开始门栓关闭,所有线程都需要等待,然后开始倒计时,倒计时为 0 后,门栓打开,等待的所有线程都可以通过
一次性,打开后就不能再关上了
参与线程有不同角色,有的负责倒计时,有的在等待倒计时;负责倒计时和等待倒计时的线程都可以有多个,用于不同角色线程之间的同步
场景
- 同时开始
- 主从协作
CyclicBarrier
循环栅栏
特点
适用于并行迭代计算,每个线程负责一部分计算,然后在栅栏处等待其它线程完成,所有线程到齐后,交换数据和计算结果,再进行下一次迭代
可以重复利用
参与线程角色是一样的,用于同一角色线程间的协调一致
小结
- 在读多写少的场景中使用
ReentrantReadWriteLock
替代ReentrantLock
,以提高性能 - 使用
Semaphore
限制对资源的并发访问数 - 使用
CountDownLatch
实现不同角色线程间的同步 - 使用
CyclicBarrier
实现同一角色线程间的协调一致
ThreadLocal
每个线程都有同一个变量的独有拷贝
不同线程访问的虽然是同一个变量,但每个线程都有自己的独立的值
方法
T get()
获取值void set(T value)
设置值T initialValue()
提供初始值,默认实现是返回 nullT setInitialValue()
设置初始值,返回之前的初始值-
void remove()
删除当前线程对应的值删除后,当再次调用
get
方法时,返回初始值
实现原理
每个线程都有一个 Map ,对于每个ThreadLocal
对象,调用其get
set
方法,实际上就是以ThreadLocal
对象为键读写当前线程的 Map
小结
ThreadLocal
常用于存储上下文信息,避免在不同代码间来回传递,简化代码
ThreadLocal
是实现线程安全、减少竞争的一种方案
在线程池中使用ThreadLocal
,需要确保初始值是符合期望的
并发总结
线程安全的机制
线程表示一条单独的执行流,每个线程都有自己的执行计数器,有自己的栈,但可以共享内存
共享内存是实现线程协作的基础
共享内存
两个问题
- 竞态条件
- 内存可见性
解决方法
- 使用
synchronized
- 使用显式锁
- 使用
volatile
- 使用原子变量和CAS
- 写时拷贝
- 使用
ThreadLocal
线程的协作机制
协作场景
- 生产者/消费者协作模式
- 主从协作模式
- 同时开始
- 集合点
机制
-
wait
/notify
- 显式条件
- 线程的中断
- 协作工具类
- 阻塞队列
-
Future
/FutureTask
容器类
同步容器
基于普通容器返回线程安全的同步容器,如SynchronizedList
并发容器
- 写时拷贝的
CopyOnWriteArrayList
和CopyOnWriteArraySet
ConcurrentHashMap
- 基于
SkipList
的ConcurrentSkipListMap
和ConcurrentSkipListSet
- 各种队列,如
ConcurrentLinkedQueue
,BlockingQueue
任务执行服务
任务的提交 和 任务的执行 相分离
实现机制:线程池
CompletionService
定时任务