搞定Java多线程、并发编程
2020-05-25 14:09:35 2 举报
AI智能生成
多线程知识点总结
作者其他创作
大纲/内容
并发编程
相关理论
重排序
为了提高程序执行性能,编译器(编译期)和 CPU(运行期)会对指令进行重排序
三大特性
原子性
线程对共享变量的操作是原子性操作,就是说该操作或多个操作,要么都成功要么都失败
对于复合操作而言,synchronized 关键字可以保证原子性,而 volatile 关键字不能
可见性
线程对共享变量的修改对其它线程立即可见
synchronized 和 volatile 关键字都能保证可见性
有序性
程序代码执行的结果不受JVM指令重排序的影响
由于 volatile 关键字可以禁止指令重排序,因此能保证有序性。
Lock 锁机制可以同时保证以上三个特性
并发编程容易出问题的三方面
1.安全性问题:存在竞态条件。所谓竞态条件,指的是程序的执行结果依赖线程执行的顺序。
活跃性问题:指的是某个操作无法执行下去。我们常见的“死锁”就是一种典型的活跃性问题,当然除了死锁外,还有两种情况,分别是“活锁”和“饥饿”。
活锁:有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁”。可以类比现实世界里的例子,路人甲从左手边出门,路人乙从右手边进门,两人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果是两人又相撞了。这种情况,基本上谦让几次就解决了,因为人会交流啊。可是如果这种情况发生在编程世界了,就有可能会一直没完没了地“谦让”下去,成为没有发生阻塞但依然执行不下去的“活锁”
“饥饿”指的是线程因无法访问所需资源而无法执行下去的情况。“不患寡,而患不均”,如果线程优先级“不均”,在 CPU 繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。
性能问题。
解决手段
既然使用锁会带来性能问题,那最好的方案自然就是使用无锁的算法和数据结构了。
减少锁持有的时间。互斥锁本质上是将并行的程序串行化,所以要增加并行度,一定要减少持有锁的时间。
JMM
Java Memory Model,即 Java 内存模型
关键概念
共享内存
即主存,所有线程共享。
(线程)本地内存
也称为“工作内存”。JVM 给每个线程都分配了一块内存区域,该块内存是线程独有的。
从架构上看,类似于计算机硬件架构中 CPU 与内存间的高速缓存
JMM 规定:线程不能直接操作主存,而是只操作属于自己的那部分内存。如果多个线程间需要进行变量共享,必须经过主存。
由于 JMM 的限制,线程操作变量都要经过以下几个基本步骤:
1、从主存中读取变量放入工作内存;
2、在工作内存中对变量进行修改操作;
3、将操作后的结果同步回主存。
1、从主存中读取变量放入工作内存;
2、在工作内存中对变量进行修改操作;
3、将操作后的结果同步回主存。
Happens-Before 原则
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在Happens-Before关系。
和程序猿相关的六条规则
1.程序的顺序性规则
在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作
2.volatile变量规则
对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
3.传递性
如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
4.管程中锁的规则
指管程中对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
5.线程start()规则
这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
6.线程join()规则
这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
线程安全问题
衡量标准:如果一段程序在多线程环境下多次执行的结果总是与预期一致,就说是线程安全的,反之则是线程不安全
所谓的线程安全问题,其本质在于线程对共享变量操作的原子性、可见性、有序性不能同时满足,因此解决线程安全问题的关键就在于使其同时满足该三个特性。
Unsafe 类
该类提供了在 Java 中能够像 C/C++ 语言使用指针直接操作内存的功能,由于直接操作内存可能会发生风险,因此Java官方不建议直接使用该类,也没有提供关于该类的文档。但是在 JDK 源码中为了提高运行效率在很多地方都使用了该类。
包含的功能
内存管理
包括分配内存、释放内存、获取变量内存地址偏移量、通过变量地址偏移量获取和修改变量值等
线程挂起与恢复
park、unpark 方法
CAS 无锁技术
CAS 即 Compare And Swap 的缩写,比较并交换。属于乐观锁的一种实现方式。
通过 JNI 调用 CPU 的 CAS 指令实现,在硬件层面实现了原子操作。
它是并发包中的锁机制和原子操作类实现的底层基础
实现原理
CAS 有3个操作数,内存值 V,旧的预期值 A,要修改的新值 B 。
当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。
若操作成功则返回结果,否则会进行重试直到成功为止。
当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。
若操作成功则返回结果,否则会进行重试直到成功为止。
缺点
只能保证对一个共享变量进行原子操作
如果要多个共享变量的原子操作,需要用到锁机制
解决办法
并发包中引入了针对引用类型的原子操作类(如 AtomicReference、AtomicReferenceArray)。
因此我们可以将多个变量放到同一个对象中,然后用引用类型的原子操作类来操作也可以保证原子性。
因此我们可以将多个变量放到同一个对象中,然后用引用类型的原子操作类来操作也可以保证原子性。
ABA问题
当一个值从 A 变为 B ,又从 B 变回了 A,这种情况下,CAS 会认为值没有发生过变化,但实际上是有变化的。
解决办法
并发包中的 AtomicStampedReference 类引入了一种基于版本号的机制,来保证对于引用类型的CAS操作的原子性。
高并发时效率降低
由于存在自旋锁(spin lock)机制,会导致CAS的失败次数会增多,从而引起更多线程的重试
解决办法
在高并发情况下,使用Java 8中新增的四个类:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder
ThreadLocal (线程本地变量)
它为每个线程都提供一个独立的变量副本,各个线程都可以改变自己的变量副本,各个线程间互不影响。
实现的思路:
1)在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap,用于存储每一个线程的变量副本,键为当前 ThreadLocal 对象,而值则是对应线程的变量副本;
2)每个 ThreadLocal 实例都有一个唯一的 threadLocalHashCode(这个值将会用于在 ThreadLocalMap 中找到 ThreadLocal 对应的 value 值)。
1)在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap,用于存储每一个线程的变量副本,键为当前 ThreadLocal 对象,而值则是对应线程的变量副本;
2)每个 ThreadLocal 实例都有一个唯一的 threadLocalHashCode(这个值将会用于在 ThreadLocalMap 中找到 ThreadLocal 对应的 value 值)。
工作原理:
1)在 Thread 类中维护一个 ThreadLocalMap 类型的属性 threadLocals(与线程进行绑定,避免了线程安全问题);
2)取值 get 操作:
① 首先从 ThreadLocal 中取变量值时先获取当前线程,然后获得当前线程的 threadLocals 属性(即与当前线程关联的 ThreadLocalMap);
② 然后再从 ThreadLocalMap 中根据当前 ThreadLocal 获取到其中的变量值(如果 ThreadLocalMap 为空,则返回初始化方法 initialValue() 的值,默认是 null,可通过重写该方法自定义初始值);
3)set 操作:找到当前线程关联的ThreadLocalMap,将当前 ThreadLocal 对象为 key,以变量值为value存入ThreadLocalMap中(如果ThreadMap为null,则创建一个新的ThreadLocalMap对象用来存储)。
4)remove 操作:从 ThreadLocalMap 中根据以当前 ThreadLocal 对象为 key 删除对应的Entry。
1)在 Thread 类中维护一个 ThreadLocalMap 类型的属性 threadLocals(与线程进行绑定,避免了线程安全问题);
2)取值 get 操作:
① 首先从 ThreadLocal 中取变量值时先获取当前线程,然后获得当前线程的 threadLocals 属性(即与当前线程关联的 ThreadLocalMap);
② 然后再从 ThreadLocalMap 中根据当前 ThreadLocal 获取到其中的变量值(如果 ThreadLocalMap 为空,则返回初始化方法 initialValue() 的值,默认是 null,可通过重写该方法自定义初始值);
3)set 操作:找到当前线程关联的ThreadLocalMap,将当前 ThreadLocal 对象为 key,以变量值为value存入ThreadLocalMap中(如果ThreadMap为null,则创建一个新的ThreadLocalMap对象用来存储)。
4)remove 操作:从 ThreadLocalMap 中根据以当前 ThreadLocal 对象为 key 删除对应的Entry。
注:
1、ThreadLocal 的目的不是解决线程共享变量问题,而是隔离线程。每个线程访问的都是属于自己的变量,因此避免了线程安全问题。
2、ThreadLocal 适用于变量需要在线程间隔离而在方法间需要共享的场景。
1、ThreadLocal 的目的不是解决线程共享变量问题,而是隔离线程。每个线程访问的都是属于自己的变量,因此避免了线程安全问题。
2、ThreadLocal 适用于变量需要在线程间隔离而在方法间需要共享的场景。
内存泄漏问题
原因
解决办法
当我们使用完ThreadLocal变量的时候,记得调用其remove()方法,以避免内存泄漏
J.U.C
线程池 (Executor体系)
线程池的作用
1、减少资源消耗。通过重复利用池中已创建的线程,减少频繁创建、销毁线程带来的资源消耗。
2、提高响应速度。当线程池中有空闲线程,任务到来时无需创建线程就能立即被执行。
3、提高线程的可管理性。由线程池对池中的线程进行统一的管理和监控,可以防止无限制创建线程造成的资源浪费。
2、提高响应速度。当线程池中有空闲线程,任务到来时无需创建线程就能立即被执行。
3、提高线程的可管理性。由线程池对池中的线程进行统一的管理和监控,可以防止无限制创建线程造成的资源浪费。
Executor 接口
其中只有一个 void execute(Runnable task) 方法,用于执行任务
ExecutorService 接口
Executor 接口的子接口
扩展了 Executor 的功能,提供了管理线程的方法,并且提供了一系列能返回 Futrue 对象的异步任务创建方法
常用方法
shutdown()、shutdownNow()
通过关闭线程池来拒绝处理新任务
shutdown 与 shutdownNow 的区别在于后者会立即关闭,不会等待正在执行的任务执行完毕
isShutdown()、isTerminated()
获取线程池是否被关闭的状态
<T> Futrue<T> submit(Callable<T> task)
<T> Futrue<?> submit(Runnable task)
<T> Futrue<T> submit(Runnable task,T result)
<T> Futrue<?> submit(Runnable task)
<T> Futrue<T> submit(Runnable task,T result)
向线程池中提交一个任务,并返回一个 Futrue 对象用于获取执行结果
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
向线程池提交一个任务集合,执行集合中的一个或多个任务,并返回执行一个或多个 Futrue
ThreadPoolExecutor 类
是线程池的真正实现,通过在构造方法传入不同的配置参数来创建不同的线程池。
核心配置参数
corePoolSize
核心线程数
线程池中存活的最少线程数量,除非 allowCoreThreadTimeout 属性值被设成 true 那么最少就是0
maximumPoolSize
最大线程数
线程池中最多能容纳的线程数量
keepAliveTime
指定空闲线程的存活时间,目的是为了减少资源消耗
在以下两种情况下生效:
1、当线程池中线程数大于 corePoolSize,多余的空闲线程达到 keepAliveTime 指定的时间时会被停掉;
2、当 allowCoreThreadTimeOut 值为 true,核心线程空闲时间达到 keepAliveTime 指定的时间时也会被停掉
1、当线程池中线程数大于 corePoolSize,多余的空闲线程达到 keepAliveTime 指定的时间时会被停掉;
2、当 allowCoreThreadTimeOut 值为 true,核心线程空闲时间达到 keepAliveTime 指定的时间时也会被停掉
workQueue
任务队列,BlockingQueue 类型
用于存放待执行的任务,可以是有界队列或无界队列
handler
任务拒绝策略,RejectedExecutionHandler 类型
用于处理线程池无法执行的任务
4种实现
AbortPolicy
默认策略,直接抛出
RejectedExecutionException
异常 CallerRunsPolicy
使用调用线程去处理任务
DiscardPolicy
什么事都不干,相当于丢弃任务
DiscardOldestPolicy
移除队列头部的任务,然后再次尝试执行当前任务
当以下两种情况发生时,就会执行任务拒绝策略:
① 任务队列满、同时达到最大线程数 maximumPoolSize;
② 线程池已经被 shutdown,无法执行新任务。
① 任务队列满、同时达到最大线程数 maximumPoolSize;
② 线程池已经被 shutdown,无法执行新任务。
工作原理
线程池被创建的时候里面是没有线程的,除非调用了 prestartAllCoreThreads() 方法。
1、当有任务需要执行并且线程池中线程数量小于 corePoolSize 时,新创建一个线程去执行这个任务。
2、当线程数量达到 corePoolSize,并且小于 maximumPoolSize 时,分两种情况:
1)若任务队列未满,则将后续任务放入任务队列;
2)若任务队列已满,则新创建一个线程去执行任务,直到达到最大线程数 maximumPoolSize;
3、达到最大线程数 maximumPoolSize 后,后续来的任务将会被执行拒绝任务策略。
1、当有任务需要执行并且线程池中线程数量小于 corePoolSize 时,新创建一个线程去执行这个任务。
2、当线程数量达到 corePoolSize,并且小于 maximumPoolSize 时,分两种情况:
1)若任务队列未满,则将后续任务放入任务队列;
2)若任务队列已满,则新创建一个线程去执行任务,直到达到最大线程数 maximumPoolSize;
3、达到最大线程数 maximumPoolSize 后,后续来的任务将会被执行拒绝任务策略。
Fork/Join (多线程并行框架ForkJoinPool)
JDK1.7引入的支持多核CPU的多线程并行框架,以充分利用多核CPU的优势。
通过它可以实现多线程在多个CPU核心中并行处理任务。
通过它可以实现多线程在多个CPU核心中并行处理任务。
利用分而治之的思想,将大任务分成小任务执行,然后合并结果;分别对应 fork、join 两个操作。
Fork/Join 框架的核心是 ForkJoinPool 类,它是对 AbstractExecutorService 类的扩展。
ForkJoinPool 实现了工作窃取(work-stealing)算法,并可以执行 ForkJoinTask 任务。
ForkJoinPool 实现了工作窃取(work-stealing)算法,并可以执行 ForkJoinTask 任务。
Executors 工具类
包含一系列创建线程池的工厂(静态)方法,简化线程池的创建
常用方法
Callable<Object> callable(Runnable task)
<T> Callable<T> callable(Runnable task, T result)
<T> Callable<T> callable(Runnable task, T result)
包装 Runnable 类型的线程为 Callable 类型
newSingleThreadExecutor()
创建一个只有单个工作线程的线程池,使用的队列是无界队列
newFixedThreadPool(int nThreads)
创建固定大小的线程池
newCachedThreadPool()
创建一个可根据需要创建线程的线程池,但是可以重用已经创建好的线程。
注:它使用的是 SynchronousQueue作为任务队列
注:它使用的是 SynchronousQueue作为任务队列
newScheduledThreadPool(int corePoolSize)
创建一个指定大小的线程池,用于定时或周期性的执行任务
newSingleScheduledThreadExecutor()
创建一个单线程化的线程池,用于定时或周期性的执行任务
newWorkStealingPool()
newWorkStealingPool(int parallelism)
newWorkStealingPool(int parallelism)
Java 8 新增,创建使用 Fork/Join 框架执行任务的线程池
线程池大小配置
通过 Runtime.getRuntime().availableProcessors() 获得当前 CPU 个数
1、如果是 CPU 密集型任务,就需要尽量压榨 CPU,参考值可以设为 NCPU + 1
+1原因:当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上
2、如果是 IO 密集型任务,参考值可以设置为CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
并发容器
ConcurrentHashMap
由于 HashMap 在多线程并发 put 操作情况下的 rehash 会导致死循环,Java 7 引入了分段锁技术 Segment 来保证一定的线程安全,Segment 继承于 ReentrantLock
为了进一步提高并发性及访问效率,Java 8 采用 CAS 替代了 Segment,同时加入了红黑树(当链表中结点数达到临界值8时就将其转成红黑树)
BlockingQueue
阻塞队列:
1)当队列为空时,获取元素的线程会等待,直到队列中有元素;
2)当队列满时,存储元素的线程会等待,直到队列可以存储元素。
通常用于生产者-消费者场景
1)当队列为空时,获取元素的线程会等待,直到队列中有元素;
2)当队列满时,存储元素的线程会等待,直到队列可以存储元素。
通常用于生产者-消费者场景
ArrayBlockingQueue
有界队列,即队列容量有限,内部是数组结构
LinkedBlockingQueue
既可以是有界队列,又可以是无界队列,内部是单向链表结构。若创建时不指定容量,则默认是 Integer.MAX_VALUE
SynchronousQueue (同步队列)
不保存元素,仅仅作为两个线程进行单向数据通信(如生产者/消费者场景)的临时通道
PriorityBlockingQueue (优先级阻塞队列)
属于无界队列,队列中的元素按优先级排列
使用了和 PriorityQueue 一样的排序规则,同时具备 BlockingQueue 的特性
通过在构造方法传入 Comparator 比较器来决定元素顺序;
如果不提供比较器,则采用自然排序(即通过 Comparable 的 compareTo方法,因此插入队列中的元素必须实现 Comparable 接口)
如果不提供比较器,则采用自然排序(即通过 Comparable 的 compareTo方法,因此插入队列中的元素必须实现 Comparable 接口)
CopyOnWriteArrayList
从字面上看就是”写时复制“。原理是当需要对集合中元素进行增删改操作时首先复制一个副本,对副本进行操作
适用于“读多写少”的并发场景
......
并发工具
ReentrantLock
Lock 用于解决互斥问题,Condition 用于解决同步问题。
用锁的最佳实践
1.永远只在更新对象的成员变量时加锁
2.永远只在访问可变的成员变量时加锁
3.永远不在调用其他对象的方法时加锁
CountDownLatch (倒计数器)
利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他几个任务执行完毕之后才能执行,就可以使用 CountDownLatch 来实现。
内部使用了 AQS 的共享锁机制
两个关键方法:
1. await() 当前线程等待其它线程执行完毕(即计数器减到0),当前线程恢复执行;
2. countDown() 当有一个线程执行完毕就将计数器减1,直至减到0。
1. await() 当前线程等待其它线程执行完毕(即计数器减到0),当前线程恢复执行;
2. countDown() 当有一个线程执行完毕就将计数器减1,直至减到0。
应用场景
模拟多个线程并发
CyclicBarrier(回环栅栏)
通过它可以实现让一组线程等待至某个状态之后再全部同时执行。
叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用(通过调用 reset() 方法)。
叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用(通过调用 reset() 方法)。
await() 方法,此方法作用是等待其它线程都到同一状态时开始同时执行。类似于长跑比赛中,当所有运动员都准备完毕才开始比赛。
Semaphore (信号量)
用于限制访问的最大线程数
维护一个访问许可集,其大小由初始化时的构造器参数指定。
主要方法
通过 acquire 和 release 方法获取和释放访问许可。其获取的方式有公平和非公平两种,默认是非公平
acquire(int permits)、tryAcquire(int permits)
获取指定数量的访问许可,相当于在可用的数量上减去
release(int permits)、tryRelease(int permits)
释放指定数量的访问许可,相当于在可用的数量上增加
Exchanger (交换器)
用于成对的线程间进行数据交换,可以看成是一个双向的 SynchronousQueue
V exchange(V x) 方法
V exchange(V x, long time, TimeUnit unit) 方法
原子操作类 (atomic子包)
位于 java.util.concurrent.atomic 包,使用 CAS 实现的原子操作。
基本类型
如 AtomicInteger 、AtomicLong 、AtomicDouble 等
数组类型
如 AtomicIntegerArray、AutomicDoubleArray等
引用类型
如AtomicReference、AtomicReferenceFieldUpdater等
比 synchronized 控制的粒度更细、量级更轻(所采用的自旋锁是一种轻量级锁),并且在多核处理器具有高性能
Java 8 又增加了四个类
DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder
弥补原有的 Atomic 系列类的不足:
虽然通过CAS保证了并发时操作的原子性,但是在高并发情况下,由于存在自旋锁(spin lock)机制,会导致CAS的失败次数会增多,从而引起更多线程的重试,最后导致效率降低。
虽然通过CAS保证了并发时操作的原子性,但是在高并发情况下,由于存在自旋锁(spin lock)机制,会导致CAS的失败次数会增多,从而引起更多线程的重试,最后导致效率降低。
LongAdder VS AtomicLong
较低并发时两个类效果差不多,而高并发时使用 LongAdder 更高效,但也会消耗更多的空间
显示锁 (locks子包)
Condition/ConditionObject
提供了类似 Object 类中 wait、notify、notifyAll 的方法,主要包括 await、signal、signalAll 方法
与 synchronized 和 wait、notify、notifyAll 的搭配类似,这些方法与 Lock 锁配合使用也可以实现等待/通知机制
LockSupport工具类
用于操作线程,是锁和同步类的基础。基于 Unsafe 中的 park、unpark 实现
用来代替 wait、notity、notifyAll 。
因为 LockSupport 对 park 方法和 unpark 方法的调用没有先后的限制,而 wait 方法则必须保证在 notify/notifyAll 之前被调用。
因为 LockSupport 对 park 方法和 unpark 方法的调用没有先后的限制,而 wait 方法则必须保证在 notify/notifyAll 之前被调用。
park()、parkNanos(long timeout)、parkUntil(long deadLine) 方法表示将当前线程挂起,后两个表示挂起一段时间
unpark(Thread thread) 方法取消指定线程 thread 挂起
JDK 1.6 后增加了一个方法参数 blocker,表示锁的对象,在通过线程监控和分析工具来定位问题时很有用
AQS 同步器
AbstractQueuedSynchronizer 类的简称
实现了线程同步的语义,是 Java 中 Lock 锁机制的基础。
通过它可以很方便的实现同步器。
通过它可以很方便的实现同步器。
核心思想:
1、基于 volatile 类型的 state 变量,配合 Unsafe 类的 CAS 操作来实现对当前锁状态 state 进行修改;
2、内部依赖一个双向队列来完成资源获取线程的排队工作。
1、基于 volatile 类型的 state 变量,配合 Unsafe 类的 CAS 操作来实现对当前锁状态 state 进行修改;
2、内部依赖一个双向队列来完成资源获取线程的排队工作。
其中定义了两种获取锁的方式
共享方式 SHARED
同一时间可以有多个线程获得锁,多个线程共享一把锁
采用共享方式得到的锁即为共享锁
适用于多个线程同时进行读操作场景
排他方式 EXCLUSIVE
同一时间只能有一个线程获取到锁
采用排他方式得到的锁即为排他锁,也称为独占锁
适用于多个线程同时进行写操作时同步
Lock 体系
JDK 层面实现,比起 synchronized 可控性更强,弥补了 synchronized 的不足。使用后必须手动释放锁,否则可能会导致死锁
lock:如果没获取到,会一直阻塞直到成功获取到锁;
tryLock:尝试获取锁,获取到则返回true;如果没获取到则返回false,不会一直阻塞,可以指定等待时间;
unlock:释放锁。
都需要显示调用。
tryLock:尝试获取锁,获取到则返回true;如果没获取到则返回false,不会一直阻塞,可以指定等待时间;
unlock:释放锁。
都需要显示调用。
典型用法:
lock.lock();
try {
// do something with lock
} finally {
lock.unlock();
}
lock.lock();
try {
// do something with lock
} finally {
lock.unlock();
}
ReentrantLock 可重入锁
包括公平锁 FairSync、非公平锁 NonfairSync 两种实现,默认是非公平
可重入锁:是指当线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞;
公平锁:是指线程按请求获取锁的先后顺序获取锁(排队);
非公平锁:则允许在线程发出请求后立即尝试获取锁(抢占),如果尝试失败才进行排队等待
公平锁:是指线程按请求获取锁的先后顺序获取锁(排队);
非公平锁:则允许在线程发出请求后立即尝试获取锁(抢占),如果尝试失败才进行排队等待
可重入实现原理:
1、其中包含一个 Thread 类型的成员变量 exclusiveOwnerThread,表示已经获取到该锁的线程,以及一个计数器;
2、当线程获取锁成功后,该成员变量即赋值为该线程,同时计数器加一;
3、下一次再获取锁的时候,会先判断已经获取到锁的线程是否是当前线程,如果是则直接获取,同时计数器加一;
4、每一次释放锁时都将计数器减一,直到减为0,将 exclusiveOwnerThread 置为null,表示当前没有线程持有该锁。
1、其中包含一个 Thread 类型的成员变量 exclusiveOwnerThread,表示已经获取到该锁的线程,以及一个计数器;
2、当线程获取锁成功后,该成员变量即赋值为该线程,同时计数器加一;
3、下一次再获取锁的时候,会先判断已经获取到锁的线程是否是当前线程,如果是则直接获取,同时计数器加一;
4、每一次释放锁时都将计数器减一,直到减为0,将 exclusiveOwnerThread 置为null,表示当前没有线程持有该锁。
ReadWriteLock 读写锁
允许多个线程对资源同时进行读操作,或一个线程写操作,但读和写操作二者不能同时发生
ReadLock 读锁
属于共享锁实现
WriteLock 写锁
属于排他锁实现
StampedLock
是Java 8引入的锁,用于解决原有 ReadLock 的读操作阻塞写操作的问题。
支持三种模式的锁操作。分别是: 写(Writing)、读(Reading)、乐观读 (Optimistic Reading)。
其中写模式获取的是互斥锁,而读模式不是。
其中写模式获取的是互斥锁,而读模式不是。
由于它支持多种锁模式协调使用,因此并没有直接实现 Lock 或 ReadWriteLock 接口。
主要方法
锁的获取和释放
写锁:writeLock()/tryWriteLock()/tryWriteLock(time, unit)、unlockWrite(stamp)
(悲观)读锁:readLock()/tryReadLock()/tryReadLock(time, unit)、unlockRead(stamp)
乐观读锁:tryOptimisticRead()
unlock(stamp)
锁模式切换
tryConvertToWriteLock(stamp)
tryConvertToReadLock(stamp)
tryConvertToOptimisticRead(stamp)
线程与进程
线程是程序的最小执行单元
进程是操作系统进行资源分配和调度的一个基本单位
关联:一个程序至少有一个进程,一个进程又至少包含一个线程。进程中的多个线程共享进程的资源
引入线程的目的:充分利用 CPU 资源,使其可以并行处理多个任务,减少时间消耗,提高效率
线程分类
用户线程 User Thread
一般是程序中创建的线程
守护线程 Daemon Thread
为用户线程服务的线程,当所有用户线程结束时才会被终止。如JVM的垃圾回收。
通过 Thread 的 setDaemon(true) 方法将一个用户线程变成守护线程
线程的生命周期
六种状态
新建状态 New
Thread 类
实现了 Runnable 接口
run 方法,无返回值
Runnable 接口
run 方法,无返回值,通过 Thread 类或线程池来使用
Callable 接口
作为 FutureTask 构造方法参数使用
call 方法,有返回值,且可以抛出异常
call方法实际是在 Runnable 的 run 方法中被执行的
就绪状态 Runnable
调用新建线程的 start() 方法
不一定会立即运行,可能需要等待 CPU 分配时间片
阻塞状态 Blocked
线程等待synchronized隐式锁的状态
等待 Waiting
发生在调用以下几个方法时:
- 不带参数的 Object.wait()
- 不带参数的 Thread.join()
- LockSupport.park()
超时等待 Timed-Waiting
与 Waiting 状态不同在于不会一直等待,而是等待指定的时间
发生在调用以下几个方法时:
- Thread.sleep(long millis)
- Object.wait(long timeout)
- Thread.join(long timeout)
- LockSupport.parkNanos()
- LockSupport.parkUntil()
终结状态 Terminated
当线程运行完毕,即死亡
其他知识点
只要线程当前处于BLOCKED、WAITING、TIMED_WAITING 其中一种状态,线程就没有CPU的使用权
图片
Thread 类的常用方法
Thread.sleep(long millseconds) 方法
1、sleep 方法是 Thread 类的静态方法,是为了保证该操作只对当前线程有效,避免线程安全问题,其它几个常用静态方法类似。
2
2
、让当前正在运行的线程
暂时停止运行,一段时间后会继续执行。 Thread.yield() 方法
当前处于运行状态的线程主动放弃占用的CPU资源,转变为就绪状态,让其他先线程执行(让步)
join 方法
在当前线程执行过程中引入另一个线程,并且当前线程需要等待另一个线程执行完毕后才能继续执行
可用于实现多个线程顺序执行。如 t.join() 表示将当前线程阻塞,直到线程 t 执行完成当前线程才能继续执行
Thread.currentThread() 方法
获取当前正在运行的线程
线程间通信
共享变量
等待/通知机制
Object 的对象监视器方法
这三个方法依赖于对象监视器,所以必须在 synchronized 语句块内使用,否则会抛出 IllegalMonitorStateException 异常。
wait 方法
使得当前线程必须要等待(阻塞状态),等到另外一个线程调用notify()或者notifyAll()方法。
notify 方法
唤醒一个等待当前对象的锁的线程(一般是高优先级的线程)开始排队
notifyAll 方法
方法会唤醒其它所有等待当前对象的锁的线程
Lock、Condition
Lock 的 lock、tryLock、unlock、getCondition 等方法
Condition 的 await、signal、signalAll 方法
使用 volatile 关键字
数据传递
PipedInputStream/PipedOutputStream 管道流
BlockingQueue 阻塞队列
Exchanger 交换器
停止线程的方法
Thread.stop 方法:已废弃
使用一个标识来表示线程的状态,通过更改它的值来控制线程的运行和停止
interrupt 中断方法
同步关键字
synchronized
在 JVM 层面实现了对临界资源的同步互斥访问,锁的释放不用人工干预,由虚拟机自动完成。
1、在 volatile 基础上增加了互斥锁,所谓“互斥”就是同一时间只能有一个线程操作该资源;
2、在 JDK 1.5 版本以后,为了弥补 synchronized 的不足,引入了 Lock 来代替它,将同步锁对象换成了 Condition 对象,并且 Condition 对象可以有多个。
2、在 JDK 1.5 版本以后,为了弥补 synchronized 的不足,引入了 Lock 来代替它,将同步锁对象换成了 Condition 对象,并且 Condition 对象可以有多个。
用法
同步代码块
通常将外界资源作为锁的对象
synchronized(obj) {
// 同步操作代码
}
// 同步操作代码
}
用于保护外界资源不被多个线程并发修改
与同步方法比较而言,使用同步代码块的好处在于其他线程仍可以访问同步代码块以外的代码
同步方法
锁的对象是当前对象this
public synchronized void test() {
// 同步操作代码
}
// 同步操作代码
}
用于保护对象属性值不会被多个线程并发修改
同步静态方法
锁的对象是类的Class对象
public static synchronized void test() {
// 同步代码
}
// 同步代码
}
用于保护类的静态属性值不会被多个线程并发修改
缺点:
1、无法知道是否成功获取到锁
2、如果是多个线程需要同时进行读操作,一个线程读操作时其它线程只有等待
1、无法知道是否成功获取到锁
2、如果是多个线程需要同时进行读操作,一个线程读操作时其它线程只有等待
注:为了确保所有线程都能看到共享变量的最新值,因此所有执行读操作或写操作的线程都必须在同一个锁上同步。
volatile
线程每次都从主内存中读取变量,改变后再写回到主内存。其作用如下:
1、使变量在多个线程间具有可见性(volatile 变量不会被缓存到寄存器或其他对处理器不可见的地方)
2、禁止 JVM 对该变量的操作进行指令重排序(保证有序性)
1、使变量在多个线程间具有可见性(volatile 变量不会被缓存到寄存器或其他对处理器不可见的地方)
2、禁止 JVM 对该变量的操作进行指令重排序(保证有序性)
使用条件
1、对变量的写操作不依赖于变量的当前值,或者保证仅有一个线程对变量进行写操作;
2、该变量不会和其他状态变量一起被纳入不变性条件中;
3、访问变量不需要加锁。
2、该变量不会和其他状态变量一起被纳入不变性条件中;
3、访问变量不需要加锁。
应用场景
一写多读,保证变量可见性
开销较低的读写锁策略
将变量使用 volatile 关键字修饰,只在多个线程同时需要进行写操作时加锁,读操作不需要
加写锁方式
synchronized 关键字
使用 Lock
volatile VS synchronized
访问 volatile 变量不需要加锁,因此不会阻塞线程,因此它是比 synchronized 更轻量级的同步机制。
但是 volatile 不能完全替代 synchronized,因为它不能保证原子性,非线程安全,而加锁机制既可以确保可见性,又可以确保原子性。
但是 volatile 不能完全替代 synchronized,因为它不能保证原子性,非线程安全,而加锁机制既可以确保可见性,又可以确保原子性。
线程死锁
死锁产生的四个必要条件
- 互斥条件:资源不能被共享。即任一时刻一个资源只能给一个进程使用,其他进程只能等待,直到资源被占有者释放。
- 不可剥夺条件:已经分配的资源不能从相应的进程中被强制地剥夺,而只能由获得该资源的进程自愿释放。
- 请求和保持条件:已经得到资源的进程可以再次申请新的资源。
- 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。
预防死锁的方法
合理对资源进行动态分配,以避免死锁
破坏死锁产生的四个必要条件
定时任务
Timer & TimerTask 类
Timer:任务调度器,通过
TimerTask:实现了 Runnable 接口。表示需要调度的任务,里面有一个run方法定义具体的任务。
schedule(TimerTask task, long delay)等方法进行调度。
TimerTask:实现了 Runnable 接口。表示需要调度的任务,里面有一个run方法定义具体的任务。
Timer 缺点:内部是单线程,因此如果有异常产生,线程将退出,整个定时任务就会失败
线程池
ScheduledExecutorService,它是 ExecutorService 的子接口。弥补了 Timer 的缺陷
通过
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
和
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
等方法来实现定时任务调度
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
和
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
等方法来实现定时任务调度
异步任务
只需创建并启动一个线程执行该任务即可,如果需要获取执行结果,则用Callable。也可以使用线程池
Callable<V> 接口
表示一个带有返回结果的任务
通常通过 ExecutorService 来使用
Future<V> 接口
包装异步任务的返回结果
常用方法
boolean isDone():用于判断任务是否执行完成
V get():获取执行结果
FutureTask
实现了Future和Runnable接口,表示一个异步任务
CompletableFuture
0 条评论
下一页