文章目录
1. 进程与线程
进程
系统运行程序的基本单位,各进程间相互独立的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程
线程是一个比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程。线程与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。所以线程也被称为轻量级进程。
在 Java 中,当我们启动 main 方法时其实就是启动了一个 JVM 的进程,而 main 方法所在的线程就是这个进程中的一个线程,也称主线程。
2. 编写线程安全类
- 避免使用实例和静态变量;
- 使用不变的变量;
- 使用 java.util.concurrent 包中线程安全的类。
3. 并发与并行
并发
同一时间段,多个任务都在执行。并发是要有处理多个任务的能力,不一定要同时。
并行
单位时间内,多个任务同时执行。并行是要有同时处理多个任务的能力。
4. 线程状态和生命周期
线程状态
线程生命周期
- 线程刚创建好是初始状态,调用 start() 方法进入就绪状态,等待线程调度器分配资源执行;
- 分配到资源后,有可能进入等待或者需要抢锁,进入等待状态和阻塞状态,也有可能进入超时等待状态;
- 等待状态需要其他线程唤醒,阻塞状态需要抢到锁,超时等待状态需要超时时间结束;
- 被唤醒,抢到锁或超时时间结束后,线程又进入就绪状态,再次等待线程调度器分配资源执行;
- 线程执行完成,进入终止状态。
5. 多线程可能带来什么问题?
并发编程的目的就是为了能提高程序的执行效率,从而提高程序运行速度。但是并发也会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
5.1 上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态,让给其他线程使用。
上下文切换指当前任务在执行完 CPU 时间片到切换为另一个任务之前会先保存自己的状态,当下次再切换回这个任务时可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
5.2 线程死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
产生死锁条件
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:请求其他资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免线程死锁
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
6. sleep() 与 wait()
都可以暂停线程的执行。
区别点 | wait() | sleep() |
---|---|---|
锁 | 释放锁 | 没有释放锁 |
作用 | 线程间交互/通信 | 暂停执行 |
自动苏醒 | 1. 线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法 2. 使用 wait(long timeout) 超时后线程会自动苏醒 | 自动苏醒 |
7. join()
join() 会使主线程(或者说调用 join() 的线程)进入等待池并等待线程执行完毕后才会被唤醒,同一时刻并不影响处在运行状态的其他线程。底层是利用 wait() 方法实现的,当线程终止的时候会自动调用自身的 notifyAll 方法,来释放所有的资源和锁。
8. synchronized
解决的是多个线程之间访问资源的同步性,synchronized 保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
Java 6 之后 Java 官方对从 JVM 层面对 synchronized 进行优化,所以现在的 synchronized 锁效率也很不错了。
8.1 使用方式
- 修饰实例方法:对当前对象加锁,进入同步代码前要获得当前对象的锁;
- 修饰静态方法:对当前类加锁,会作用于类的所有对象。
- 修饰代码块:对指定对象加锁,进入同步代码前要获得指定对象的锁。
8.2 底层原理
-
修饰代码块
使用monitorenter
和monitorexit
指令。其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取monitor
(monitor对象存在于每个Java对象的对象头中)的持有权。当计数器为0则可以成功获取,获取后将锁计数器加1。在执行 monitorexit 指令后,将锁计数器减1。当计数器为0时表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 -
修饰方法
使用ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,JVM 通过该标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
8.3 JDK 6 优化
JDK 6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁粗化和锁消除等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,锁状态会随着竞争的激烈而逐渐升级。但锁状态只能升级而不能降级,为了提高获得锁和释放锁的效率。
8.3.1 锁升级
8.3.1.1 偏向锁
偏向锁意思是锁会偏向于第一个获得它的线程,如果在接下来该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的。偏向锁失败,会先升级为轻量级锁。
8.3.1.2 轻量级锁
轻量级锁是为了在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统锁而产生的性能消耗。轻量级锁通过 CAS 来实现加锁和解锁。
轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”。如果没有竞争,轻量级锁使用 CAS 操作避免了使用操作系统锁而带来的开销。但如果存在锁竞争,除了操作系统锁开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢。
引入偏向锁的目的和轻量级锁的目的很像,都是为了在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统锁而产生的性能消耗。但是不同是:偏向锁在无竞争的情况下会把整个同步都消除掉,而轻量级锁在无竞争的情况下使用 CAS 操作。
8.3.1.3 自旋锁和自适应自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行自旋锁的优化手段。通过让一个线程执行一个忙循环(自旋),看看持有锁的线程是否很快就会释放锁。
因为同步对性能最大的影响就是阻塞,而挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。
自旋锁在 JDK 6 之前其实就已经引入了,不过是默认关闭的,需要通过 --XX:+UseSpinning
参数来开启。JDK 6 及 6 之后,就改为默认开启的了。自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。自旋等待的时间是有限度的,如果自旋超过了限定次数仍然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,可以通过 --XX:PreBlockSpin
来更改。
在 JDK 6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进是:自旋的时间不在固定了,而是由前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定。
8.3.2 锁粗化
我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小。在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。锁粗化可以在有些情况下把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
8.3.3 锁消除
锁消除指的就是虚拟机在编译器运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省请求锁的时间。
9. synchronized 与 ReentrantLock
都是可重入锁
可重入锁:自己可以再次获取自己的内部锁。
比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 而实现。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)。
ReentrantLock 比 synchronized 增加了一些高级功能
- 等待可中断。通过
lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现公平锁。synchronized 只能是非公平锁。ReentrantLock 默认情况是非公平的,可通过
ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 - 选择性通知(锁可以绑定多个条件)。synchronized 关键字与 wait() 和 notify()/notifyAll() 方法相结合可以实现等待/通知机制;ReentrantLock 类需要借助于 Condition 接口与 newCondition() 方法来实现选择性通知。
10. volatile 关键字
可见性
在 JDK 2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
防止重排序
分配静态变量步骤:
- 为 静态变量 分配内存空间
- 初始化 静态变量
- 将 静态变量 指向分配的内存地址
JVM 具有指令重排的特性,所以执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。
volatile 保证多线程下的可见性的同时,禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
11. ThreadLocal
ThreadLocal 实现每一个线程都有自己专属的本地变量。创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本。
原理
ThreadLocal 内部维护的是一个类似 Map 的数据结构 ThreadLocalMap,key 为当前对象的 Thread 对象,值为 Object 对象。最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。 ThrealLocal 类中可以通过 Thread.currentThread() 获取到当前线程对象后,直接通过 getMap(Thread t) 可以访问到该线程的 ThreadLocalMap 对象。
内存泄露问题
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这就导致 ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。
ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后最好手动调用 remove() 方法。
12. Runnable 和 Callable
Runnable 自 JDK 1 以来一直存在,但 Callable 仅在 JDK 5 中引入,目的就是为了来处理 Runnable 不支持的用例。
Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。Executors.callable(Runnable task)
或 Executors.callable(Runnable task,Object resule)
。
13. 线程池
好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
构造方法
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
最重要的参数
- corePoolSize(核心线程数):最小可以同时运行的线程数量。
- maximumPoolSize(最大线程数):当队列中存放的任务达到队列容量的时候,当前可以同时运行的最大线程数数量。
- workQueue(等待队列):当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
设置线程池大小:
- CPU密集型:N + 1
- IO密集型:2N
- Runtime.getRuntime.avaliableProcessors()
其他常见参数
- keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁;
- unit:keepAliveTime 参数的时间单位。
- threadFactory:创建新线程的时候会用到。
- handler:饱和策略。当最大线程池满,队列也满的时候的策略。
饱和策略
- ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException 来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略会增加队列容量。如果应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
流程
如果核心线程池2,最大线程池4,等待队列2。现在要创建10个线程任务。
- 首先预启动核心线程p1,p2;
- 线程任务1,2提交过来,到p1,p2执行;
- 线程任务3,4提交过来,核心线程池满了,进去等待队列;
- 线程任务5,6提交过来,核心线程池满了,等待队列满了,线程池没满,创建线程,线程3,4执行,线程5,6进入队列;
- 线程任务7,8,9,10提交过来,核心线程池满,队列慢,线程池满,拒绝。
14. Atomic 原子类(变量锁)
Atomic 是指一个操作是不可中断的,即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。所以原子类就是具有原子操作特征的类。
基本类型
使用原子的方式更新基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
引用类型
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
- AtomicMarkableReference :原子更新带有标记位的引用类型
对象的属性修改类型
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicInteger 类的原理
AtomicInteger 类主要利用 CAS + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。并且 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
CAS是通过native方法通过硬件指令 comare and swap 保证原子执行。当有多个CPU时,这个指令也会锁住总线,确保同一时刻只有一个CPU能访问内存。
CAS存在ABA问题:假设有两个线程,线程1读到内存的数值为A,然后时间片到期,撤出CPU。线程2运行,线程2也读到了A,把它改成了B,然后又把B改成原来的值A。简单点说,修改的次序是A->B->A。然后线程1开始运行,它发现内存的值还是A,完全不知道内存中已经被操作过。
如果只是简单的数字那没什么,可是如果使用 AtomicReference, 并且操作的是复杂的数据结构,就可能会出问题了。所以可以使用 AtomicStampedReference,给每个对象都加入一个version。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
// ...
}
15. AQS(AbstractQueuedSynchronizer)
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计数器),一个是线程标记(当前线程是谁加锁的),一个是阻塞队列(用于存放其他未拿到锁的线程)。
首先,比如线程A操作了lock()方法,会先通过CAS将state赋值为1,然后将该锁标记为线程A加锁。
当线程A还未释放锁时,线程B来请求,会查询锁标记的状态。因为当前的锁标记为线程A,线程B未能匹配上,所以线程B会进入阻塞队列。直到线程A触发了 unlock() 方法,这时候线程B才有机会去拿到锁,但是不一定肯定是线程B先取到。
如果线程A触发了 unlock() 方法,会先将 state 减1变为0。当 state 为0的时候会将锁的状态清除将 state 设为 null,此时线程B可以拿到锁。
资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
- 非公平锁:无视队列顺序直接去抢锁,谁抢到就是谁的。
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock。
公平锁与非公平锁
根据上面提及的流程,如果线程A还未释放锁,线程B进来时发现当前锁标记为线程A,那么会进入阻塞队列,等待取锁。当线程C进来发现当前锁标记为线程A,也会进入阻塞队列。那么下次加锁到底是线程B先拿到还是线程C先拿到呢?
如果是公平锁,就会遵循先入先出的原则,线程B会先拿到;如果是非公平锁,无视队列顺序直接去抢锁,谁抢到就是谁的。
AQS底层使用了模板方法模式
自定义同步器时需要重写下面几个AQS提供的模板方法:
// 该线程是否正在独占资源。只有用到condition才需要去实现它。
isHeldExclusively()
// 独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryAcquire(int)
// 独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryRelease(int)
// 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryAcquireShared(int)
// 共享方式。尝试释放资源,成功则返回true,失败则返回false。
tryReleaseShared(int)
以 ReentrantLock 为例,state 初始化为0,表示未锁定状态。线程A lock() 时,会调用 tryAcquire() 独占该锁并将 state+1。此后,其他线程再 tryAcquire() 时就会失败,直到A线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,线程A自己是可以重复获取此锁的(state会累加)。因为 ReentrantLock 是可重入锁,获取多少次锁就要释放多么次锁,这样才能保证 state 回到0值。
以 CountDownLatch 以例,任务分为N个子线程去执行,state 也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后 countDown() 一次,state 会 CAS 减1。等到所有子线程都执行完后(即state=0),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。
15.1 Semaphore(信号量)
Semaphore 可以指定多个线程同时访问某个资源。
信号量是指定一定数量的许可证,执行 acquire() 阻塞获取许可,执行 release() 释放一个许可,也可以用 tryAcquire() 尝试获取。信号量有两种模式,公平和非公平。只有拿到许可证后,才能执行。
15.2 CountDownLatch 与 CyclicBarrier
15.2.1 CountDownLatch (倒计时器)
CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。它是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,就不能再次被使用。
使用场景
- 主线程等待多个子线程执行完。
初始化一个n,在主线程上 await(),每当一个线程执行完就 countDown()。当计数器为0时,主线程才被唤醒继续执行。 - 多个线程等待被同时执行。
初始化一个共享 CountDownLatch,计数器为1,多个线程开始任务前 await(),当主线程 countDown() 时,计数器变为0,多个线程同时被唤醒继续执行。
15.2.2 CyclicBarrier(循环栅栏)
CyclicBarrier 意思是可循环使用的屏障。让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达该屏障时,该屏障才会开门,所有被屏障拦截的线程才会继续执行。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
使用场景
多线程计算数据,最后合并计算结果的应用场景。
CyclicBarrier还提供一个更高级的构造函数 CyclicBarrier(int parties, Runnable barrierAction)
,用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
比如一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水。先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,await() 等待。最后,再用 barrierAction 用这些线程的计算结果,计算出整个Excel的日均银行流水。
15.2.3 区别
CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行。
CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。
区别点 | CountDownLatch | CyclicBarrier |
---|---|---|
特点 | state 为0时释放所有等待线程 | state 计数达到指定值时释放所有等待线程 |
重置 | 计数为0时,无法重置 | 计数达到指定值时,计数置为0重新开始 |
使用方式 | 调用 countDown() 方法计数减一,调用 await() 方法只进行阻塞,对计数无影响 | 调用 await() 方法计数加1,若加1后的值不等于指定值,则线程阻塞 |
重复利用 | 不可重复利用 | 可重复利用 |
15.3 ReentrantReadWriteLock(读写锁)
多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。
一个线程写入,其他线程既不能写入也不能读取;没有写入时,多个线程可同时读取。
一个线程读取,写线程要等待读线程释放锁之后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
参考:
Java多线程
Java编写线程安全类的7个技巧
JAVA多线程中join()方法的详细分析
synchronized 总结
Synchronized底层实现
全网最细:17张图带你秒杀synchronized关键字
Java锁消除和锁粗化
java公平锁,非公平锁,CAS,AQS的一些事情
公平锁和非公平锁的区别?
乐观锁、悲观锁、AQS、sync和Lock
并发编程面试必备:AQS 原理以及 AQS 同步组件总结