Java笔记之多线程和并发

本笔记来自 计算机程序的思维逻辑 系列文章

线程

创建线程的方式

  • 继承Thread
  • 实现Runnable接口

属性和方法

  • long tid 线程ID,递增整数

  • String name 线程名,默认以 Thread- + 线程编号 构成

  • int priority 优先级,范围是 1 到 10 ,默认是 5

  • int threadStatus 状态,枚举类型:NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED

  • boolean daemon 是否守护线程

    当程序中只存在daemon线程时,程序就会退出

    程序运行时,除了创建main线程,至少还会创建一个负责垃圾回收的线程,它就是daemon线程,当main线程结束时,垃圾回收线程也会退出

  • boolean isAlive() 判断线程是否存活

  • void yield() 表示当前线程不着急使用CPU

  • void 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 调用中返回

线程从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() 提供初始值,默认实现是返回 null

  • T 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

定时任务

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容