美文网首页
Java笔记之多线程和并发

Java笔记之多线程和并发

作者: 码匠 | 来源:发表于2019-01-20 18:13 被阅读0次

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

    线程

    创建线程的方式

    • 继承Thread
    • 实现Runnable接口

    属性和方法

    • long tid 线程ID,递增整数

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

    • int priority 优先级,范围是 110 ,默认是 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 等待

    把当前线程放到条件等待队列并阻塞,等待唤醒

    过程
    • 把当前线程放入条件等待队列,释放对象锁,阻塞等待,线程状态变为 WAITINGTIMED_WAITING
    • 等待时间到或被其它线程调用notify/notifyAll从条件等待队列中移除,这时,要重新竞争对象锁
      • 如果能够获得锁,线程状态变为 RUNNABLE ,并从 wait 调用中返回
      • 否则,该线程加入对象锁等待队列,线程状态变为 BLOCKED ,只有在获得锁后才会从 wait 调用中返回

    线程从wait调用中返回后,不代表其等待的条件就一定成立了,它需要重新检查其等待的条件。

    一般调用模式
    synchronized (obj) {
        while (条件不成立) {
            obj.wait();
        }
        // ... 执行条件满足后的操作
    }
    

    notify 唤醒

    从条件等待队列中移除一个线程并将之唤醒

    notifyAll 唤醒

    移除条件等待队列中所有线程并全部唤醒

    机制

    • 每个对象都有一把锁和用于锁的等待队列,还有一个条件等待队列,用于线程间的协作
    • 一个对象调用wait方法就会当前线程放到条件队列上并阻塞,表示当前线程执行不下去了,需要等待一个条件,这个条件它本身无法改变,需要其它线程改变
    • 当其它线程改变了条件后,调用该对象的notifynotifyAll方法,将其唤醒

    注意

    • waitnotify 方法只能在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

    基本用法

    该类的lockunlock方法实现了与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() 在等待该锁的线程个数
    实现原理

    依赖 CASLockSupport

    LockSupport

    • void park() 使当前线程放弃CPU,进入等待状态,操作系统不再对它进行调度,直到有其它线程对它调用了unpark
    • void parkNanos(long nanos) 指定等待的最长时间,相对时间
    • void parkUntil(long deadline) 指定最长等到什么时候,绝对时间
    • void unpark(Thread thread) 使线程恢复可运行状态

    显式条件

    显式条件与wait/notify相对应

    wait/notifysynchronized配合使用;显式条件与显式锁配合使用

    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 不存在,则设置 keyvalue ,返回之前的值

      如果 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接口,直接支持一些原子复合操作
    • 实现了SortedMapNavigableMap接口,可排序,默认按键有序,可传递比较器自定义排序
    跳表

    基于 链表 ,在链表的基础上加了多层索引结构

    高层的索引节点一定同时是低层的索引节点;高层的索引节点少,低层的多

    每个索引节点,有两个指针,一个向右,指向下一个同层的索引节点,另一个向下,指向下一层的索引节点或基本链表节点

    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() 是否调用了shutdownshutdownNow

    • boolean isTerminated() 是否所有任务都已结束

      只有调用过shutdownshutdownNow并且所有任务都已结束才返回 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内部主要由 TaskQueueTimerThread 两部分组成

    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

    并发容器
    • 写时拷贝的CopyOnWriteArrayListCopyOnWriteArraySet
    • ConcurrentHashMap
    • 基于SkipListConcurrentSkipListMapConcurrentSkipListSet
    • 各种队列,如ConcurrentLinkedQueueBlockingQueue

    任务执行服务

    任务的提交任务的执行 相分离

    实现机制:线程池

    CompletionService

    定时任务

    相关文章

      网友评论

          本文标题:Java笔记之多线程和并发

          本文链接:https://www.haomeiwen.com/subject/yxxfjqtx.html