线程协作机制
Java中多个线程之间如果需要协同工作,需要下面工具进行协同。我们分别来介绍下每个工具的作用和使用注意点。
读写锁ReentrantReadWriteLock
多个线程的读操作完全可以并行,在读多写少的场景中,让读操作并行可以明显提高性能,此时我们可以使用读写锁。
在Java并发包中,接口ReadWriteLock表示读写锁:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
写锁:只有一个线程可以进行写操作:
- 在获取写锁时,只有没有任何线程持有任何锁才可以获取到;
- 在持有写锁时,其他任何线程都获取不到任何锁。
读锁:在没有其他线程持有写锁的情况下,多个线程可以获取和持有读锁。
即:只有“读-读”操作是可以并行的,“读-写” 和 “写-写” 都不可以。
ReentrantReadWriteLock是主要实现类,可重入读写锁。实现原理:
使用同一个整数变量表示锁的状态,16位给读锁用,16位给写锁用,使用一个变量便于进行CAS操作。
锁的等待队列也只有一个。
写锁的获取:确保当前没有其他线程持有任何锁,否则就等待。
写锁释放后:将等待队列中的第一个线程唤醒,唤醒的可能是等待读锁的,也可能是等待写锁的。
读锁的获取:首先,只要写锁没有被持有,就可以获取到读锁;此外,在获取到读锁后,它会检查等待队列,逐个唤醒最前面的等待读锁的线程,直到第一个等待写锁的线程。如果有其他线程持有写锁,获取读锁会等待。
读锁释放后:检查读锁和写锁数是否都变为了0,如果是,唤醒等待队列中的下一个线程。
信号量Semaphore
有的单个资源即使可以被并发访问,但并发访问数多了可能影响性能,所以希望限制并发访问的线程数。信号量类Semaphore就是用来解决这类问题的,它可以限制对资源的并发访问数:
public Semaphore(int permits) public Semaphore(int permits, boolean fair)
permits表示许可数量,fire表示公平。主要方法:
//阻塞获取许可 public void acquire() throws InterruptedException //阻塞获取许可,不响应中断 public void acquireUninterruptibly() //批量获取多个许可 public void acquire(int permits) throws InterruptedException public void acquireUninterruptibly(int permits) //尝试获取 public boolean tryAcquire() //限定等待时间获取 public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException //释放许可 public void release()
信号量与锁的区别:
- 一般锁只能由持有锁的线程释放,而Semaphore表示的只是一个许可数,任意线程都可以调用其release方法;
- 主要的锁实现类ReentrantLock是可重入的,而Semaphore不是,每一次的acquire调用都会消耗一个许可。
实现原理:信号量的基本原理比较简单,也是基于AQS实现的,permits表示共享的锁个数,acquire方法就是检查锁个数是否大于0,大于则减一,获取成功,否则就等待,release就是将锁个数加一,唤醒第一个等待的线程。
倒计时门栓CountDownLatch
CountDownLatch相当于是一个门栓,一开始是关闭的,所有希望通过该门的线程都需要等待,然后开始倒计时,倒计时变为0后,门栓打开,等待的所有线程都可以通过,它是一次性的,打开后就不能再关上了。
public CountDownLatch(int count) public void await() throws InterruptedException public boolean await(long timeout, TimeUnit unit) throws InterruptedException public void countDown()
await检查计数是否为0,如果大于0,就等待,await可以被中断,也可以设置最长等待时间。
countDown检查计数,如果已经为0,直接返回,否则减少计数,如果新的计数变为0,则唤醒所有等待的线程。
门栓的两种应用场景:一种是同时开始,另一种是主从协作。
循环栅栏CyclicBarrier
CyclicBarrier相当于是一个栅栏,所有线程在到达该栅栏后都需要等待其他线程,等所有线程都到达后再一起通过,它是循环的,可以用作重复的同步。
CyclicBarrier特别适用于并行迭代计算,每个线程负责一部分计算,然后在栅栏处等待其他线程完成,所有线程到齐后,交换数据和计算结果,再进行下一次迭代。
public CyclicBarrier(int parties) public CyclicBarrier(int parties, Runnable barrierAction)
parties表示参与的线程个数;
Runnable表示栅栏动作,当所有线程到达栅栏后,在所有线程执行下一步动作前,运行参数中的动作,这个动作由最后一个到达栅栏的线程执行。
CyclicBarrier主要的方法就是await:
public int await() throws InterruptedException, BrokenBarrierException public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
await在等待其他线程到达栅栏。
调用await后,表示自己已经到达。
如果自己是最后一个到达的,就执行可选的命令,执行后,唤醒所有等待的线程,然后重置内部的同步计数,以循环使用。
await可以被中断,可以限定最长等待时间,中断或超时后会抛出异常。
异常BrokenBarrierException表示栅栏被破坏了:
- 在CyclicBarrier中,参与的线程是互相影响的,只要其中一个线程在调用await时被中断了,或者超时了,栅栏就会被破坏。
- 如果栅栏动作抛出了异常,栅栏也会被破坏。被破坏后,所有在调用await的线程就会退出,抛出BrokenBarrierException。
CyclicBarrier与CountDownLatch的区别:
- 线程角色
- CountDownLatch的参与线程是有不同角色的,有的负责倒计时,有的在等待倒计时变为0,负责倒计时和等待倒计时的线程都可以有多个,用于不同角色线程间的同步。
- CyclicBarrier的参与线程角色是一样的,用于同一角色线程间的协调一致。
- CountDownLatch是一次性的,而CyclicBarrier是可以重复利用的。
ThreadLocal
线程本地变量:每个线程都有同一个变量的独有拷贝。
ThreadLocal是一个泛型类,接受一个类型参数T,它只有一个空的构造方法,主要函数:
public T get() public void set(T value) protected T initialValue() public void remove()
set就是设置值,get就是获取值,如果没有值,返回null。
initialValue用于提供初始值,这是一个受保护方法,可以通过匿名内部类的方式提供,当调用get方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回null。
remove删掉当前线程对应的值,如果删掉后,再次调用get,会再调用initialValue获取初始值。
ThreadLocal对象一般都定义为static,以便于引用。
基本原理:每个线程都有一个Map,类型为ThreadLocalMap,调用set实际上是在线程自己的Map里设置了一个条目,键为当前的ThreadLocal对象,值为value。ThreadLocalMap是一个内部类,它是专门用于ThreadLocal的,与一般的Map不同,它的键类型为WeakReference<ThreadLocal>。
#java原理##并发编程#知其然知其所以然,只有掌握了底层原理,借助第一性原理,才可以在日常开发和项目中运用自如,潇洒走江湖。 专为27届毕业生准备,托起您的就业梦。 该专辑会不定时更新,建议27届同学订阅,入职后扎实的基本功可以帮您争取更好的机会和项目。

查看78道真题和解析