【Java 多线程】5 - 10 ThreadLocal 和并发包中的其他工具类
§5-10 ThreadLocal
和并发包中的其他工具类
5-10.1 ThreadLocal
线程局部变量
ThreadLocal
是一个位于 java.lang
包下的一个类,用于表示线程的局部变量。该类会为每一条线程创建自己的副本,使得每一条线程都具有自己的变量,因而得名线程的局部变量。这虽然占用了更多的空间,但由于每条线程的变量互不干扰、相互隔离,是一种解决线程安全问题的手段。
与 synchronized
和 ReentrantLock
不同,锁保护的是多线程的共享变量。而 ThreadLocal
使得每一条线程都具有自己的变量副本,适用于没一条线程都需要自己的独立实例的情况。
通常而言,会把 ThreadLocal
声明为类的 private static
变量,以供不同的线程通过该静态变量获取线程的局部变量。
构造方法:
构造方法 | 描述 |
---|---|
ThreadLocal() |
创建一个线程局部变量 |
注意:该类是一个泛型类,泛型用于指定变量的值类型,只能为引用类型。
类的方法:
方法 | 描述 |
---|---|
T get() |
获取当前线程下的线程局部变量的值 |
protected T initialValue() |
为线程局部变量返回初始值 |
void remove() |
移除当前线程局部变量的值 |
void set(T value) |
将当前线程的局部变量值赋值为新值 |
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) |
创建一个线程局部变量 |
注意:
-
线程初次获得线程局部变量后,在未赋值的情况下首次调用
get
方法取值,会先调用initialValue
方法为变量赋初值,该方法为protected
方法,是一种明显的子类覆盖设计。一般建议在声明ThreadLocal
变量的同时重写该方法,定义初值,例如:private static ThreadLocal<Integer> counter = new ThreadLocal() { @Override protected Integer initialValue() { return 0; } }
这在一些需要防止空指针异常的方法中显得尤为重要。
-
线程的局部变量实际存储在
Thread
所维护的变量threadLocals
中,这是一张哈希表,类型为ThreadLocal.ThreadLocalMap
,该表的键值对<K,V>
分别对应为<ThreadLocal, Object>
,该键值对由ThreadLocal.ThreadLocalMap.Entry
存储,一个ThreadLcoal
维护一个变量的值,存储在Object
中; -
Entry
继承了WeakReference
,父类(WeakReference
的 父类Reference
)的referent
变量存储该键值对中的键ThreadLocal
,继承关系使得ThreadLcoal
成为一个弱引用,发生 GC 时一定会被垃圾回收器回收,避免内存泄漏。但并不意味着不会发生内存泄漏,一条线程的线程局部变量引用关系为:Thread --> ThreadLocalMap --> Entry --> ThreadLocal & Object(value)
注意到,
value
并没有被声明为弱引用,这会导致其无法被回收,导致内存泄漏。为避免这一问题,在不需要使用该线程局部变量时,调用remove
方法移除变量值以解决这一问题。该部分引用自:
5-10.2 并发包中的并发集合
在集合框架一章中所介绍的所有集合类,都是线程不安全的类。在高并发场景下极其容易出现线程不安全问题。为了在并发场景下也能够使用并发集合,JUC 中提供了一些线程安全的并发集合。
并发集合接口 | 该接口的实现类(并发集合类) | 描述 |
---|---|---|
BlockingQueue |
LinkedBlockingQueue , ArrayBlockingQueue , PriorityBlockingQueue , DelayQueue ,LinkedBlockingDeque , LinkedTransferQueue , SynchronousQueue |
阻塞队列 |
TransferQueue |
LinkedTransferQueue |
传输队列 |
BlockingDeque |
LinkedBlockingDeque |
阻塞双端队列 |
ConcurrentMap |
ConcurrentHashMap , ConcurrentSkipListMap |
并发映射表 |
ConcurrentNavigableMap |
ConcurrentSkipListMap |
并发可导航映射表 |
其他并发集合 | 描述 |
---|---|
CopyOnWriteArrayList , CopyOnWriteArraySet |
写入时复制数组列表和集合 |
ConcurrentSkipListSet |
并发跳表集合 |
这些并发集合类也是集合框架的成员,提供了并发场景下线程安全的集合,由于篇幅受限,这里只挑出一些重要且常见的集合类加以说明。
5-10.2.1 BlockingQueue
阻塞队列
阻塞队列是一种额外支持等待操作的队列:等待队列非空时取出数据,等待队列具有可用空间时插入数据。
在生产者消费者模式那一篇文章中,就介绍过使用阻塞队列实现生产者消费者模式。在生产者消费者模式中,一对生产者和消费者共用同一条阻塞队列。若多个不同的生产者和消费者基于相同数据上操作,则这些消费者和生产者可以共用同一条阻塞队列。生产者尝试往队列中插入数据,消费者从队列中取出数据,这两个过程会根据队列的容量可能发生阻塞。故而得名阻塞队列。
阻塞队列也用于线程池中,用于存放等待执行的任务,又称为工作队列。线程池中的线程会不断地从工作队列中取出任务来执行,若取不到任务,空闲线程会被销毁。
阻塞队列提供了四种不同形式的插入、移除和检查的方法,以满足时在不同情况下的需要。为了满足在特殊值情况下的需要,该队列不支持插入空元素(null
)。
方法 | 抛出异常 | 返回特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入元素 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
移除元素 | remove() |
poll() |
take() |
take(time, unit) |
检查元素(取出但不移除) | element() |
peek() |
不适用 | 不适用 |
该接口具有子接口 BlockingDeque
阻塞双端队列和 TranserQueue
传输队列。前者提供了双端队列的操作,后者用于生产者等待消费者获取元素(提供查询消费者的方法)。这两个子接口各自都只有一个实现类,分别为 LinkedBlockingDeue
和 LinkedTransferQueue
。
而直接实现 BlockingQueue
的实现类有很多,这里简要地介绍一下:
ArrayBlockingQueue
数组阻塞队列:基于数组的有界先入先出阻塞队列。位于队列头的元素总是队列中时间最长的,而位于队列尾的元素总是队列中时间最短的;LinkedBlockingQueue
链表阻塞队列:基于链表的可有界先入先出阻塞队列。位于队列头的元素总是队列中时间最长的,而位于队列尾的元素总是队列中时间最短的。与数组阻塞队列相比,链表阻塞队列具有更好的吞吐量,但在大多数并发应用中性能更不可预测;DelayQueue
延时队列:一个无界的阻塞队列。该队列存储Delayed
接口的实现类,延时超时后原素材能够被移除。该接口是函数式接口,有且只有一个方法getDelay(TimeUnit)
,返回指定时间单位下该对象的剩余延时。超时时间小于等于 0 纳秒的元素视为超时元素,具有最短超时时间的元素视为队列头。只有移除元素的方法会相应元素超时时间,只有超时队列头才能够被移除,否则,方法将会阻塞直至队列头超时。剩余方法不考虑元素超时;PriorityBlockingQueue
优先级阻塞队列:基于优先级堆的无界阻塞队列。元素按照优先级排序,默认情况下,按照自然排序顺序排序(元素实现Comparable
接口),或调用构造器中指定的比较器当作优先级排序。队列中的元素必须实现Comparable
接口,且不允许空元素插入队列。根据指定排序方式,队列中最小的元素视为队列头,取元素操作访问的是队列中的头元素;SynchronousQueue
同步队列:一个不存储任何元素的无界阻塞队列,每一个插入操作都必须等待一个对应的删除操作,反之亦然;
上述阻塞队列应当视不同情况酌情选择。
BlockingQueue
继承自 Queue
,支持 Queue
接口的方法,这里不做说明。这里仅列出阻塞队列的其他方法:
方法 | 描述 |
---|---|
int drainTo(Collection<? super E> c) |
将队列中的所有元素移除,并转移至所给集合中,返回转移的元素个数 |
int drainTo(Collection<? super E> c, int maxElements) |
将队列中最多指定个数元素移除,并转移至所给集合中,返回转移的元素个数 |
阻塞队列的阻塞方法源码分析:以 ArrayBlockingQueue
为例。
添加元素。
public void put(E e) throws InterruptedException {
// 要求元素非空,否则抛出异常 NullPointerException
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
// 可中断取锁,线程被中断时取消取锁
lock.lockInterruptibly();
try {
// 典型的生产者模型
// 若队列已满,则开始等待,进入阻塞状态
while (count == items.length)
notFull.await();
// 队列具有可用空间,将元素插入队列中
// 该方法内部会调用 notEmpty.signal() 唤醒消费者
enqueue(e);
} finally {
lock.unlock();
}
}
取出元素。
public E take() throws InterruptedException {
// 可中断取锁,线程被中断时取消取锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 典型的消费者模型
// 若队列为空,则开始等待,进入阻塞状态
while (count == 0)
notEmpty.await();
// 队列具有元素,取出元素
// 方法内部会调用 notFull.signal() 唤醒生产者
return dequeue();
} finally {
lock.unlock();
}
}
5-10.2.2 ConcurrentHashMap
并发映射表
在集合框架一章中所介绍的所有集合都是线程不安全的,但若想要获得对应的线程安全集合类,可以通过 Collections
工具类获取:
List syncArrayList = Collections.synchronizedList(new ArrayList<>());
List syncLinkedList = Collections.synchronizedList(new LinkedList<>());
Set syncHashSet = Collections.synchronizedSet(new HashSet<>());
Set syncLinkedHashSet = Collections.synchronizedSet(new LinkedHashSet<>());
Set syncTreeSet = Collections.synchronizedSet(new TreeSet<>());
Map syncHashMap = Collections.synchronizedMap(new HashMap<>());
Map syncLinkedHashMap = Collections.synchronizedMap(new LinkedHashMap<>());
Map syncTreeMap = Collections.synchronizedMap(new TreeMap<>());
这些方法会对应返回 SynchronizedList
, SynchronizedSet
, SynchronizedMap
的实例,这些类都是 Collections
的内部类,其线程安全的实现方式是使用了 synchronized
关键字。但其所有方法都是通过监视器锁保证线程安全,在高并发场景下可能会折损性能。
为了满足高并发场景下的使用需求,使用 ConcurrentHashMap
更好。下面来看看该类的使用方法。
构造方法:
构造方法 | 描述 |
---|---|
ConcurrentHashMap() |
构造一个新的空映射表,具有默认初始大小(16),默认加载因子(0.75) |
ConcurrentHashMap(int initialCapacity) |
构造一个新的空映射表,具有指定初始大小,默认加载因子(0.75) |
ConcurrentHashMap(int initialCapacity, float loadFactor) |
构造一个新的空映射表,具有指定初始大小,指定加载因子 |
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) |
构造一个新的空映射表,具有指定初始大小和指定加载因子,并指定并发等级(并发修改线程数) |
ConcurrentHashMap
的一些参数的语义同 HashMap
,由于该映射表支持并发修改,该类还具有字段 concurrencyLevel
指定并发修改线程数(并发级别)。
常用方法:
方法 | 描述 |
---|---|
void clear() |
移除映射表中的所有映射关系 |
boolean isEmpty() |
测试映射表是否为空 |
boolean contains(Object value) |
测试映射表中是否由键映射到指定的值 |
boolean containsKey(Object key) |
测试映射表中是否含有指定的键 |
boolean containsValue(Object value) |
测试映射表中是否含有指定的值 |
Set<Map.Entry<K,V>> entrySet() |
返回含有映射表中所有键值对的集合 |
ConcurrentHashMap.KeySetView<K,V> keySet() |
返回含有银蛇表中所有键的集合 |
long mappingCount() |
返回映射关系数 |
V put(K key, V value) |
向映射表中添加映射关系(具有覆盖行为) |
void putAll(Map<? extends K, V extends V> m) |
向映射表中添加另一映射表中所有的映射关系 |
V get(Object key) |
返回映射表中指定键的映射值 |
void forEach(long parallelismThreshold, BiConsumer<? super K, ? super V> action) |
为每一个键值对施加操作,并行度阈值指定该操作并行执行时所需的元素数量(预计值) |
一个十分简单的示例:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(100);
new Thread(() -> {
for (int i = 0; i < 50; i++) {
map.put(i + "", i + "");
}
}).start();
new Thread(() -> {
for (int i = 25; i < 51; i++) {
map.put(i + "", i + "");
}
}).start();
try {
// 让主线程休眠,等待添加操作完成
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(map);
}
}
结果:
{10=10, 11=11, 12=12, 13=13, 14=14, 15=15, 16=16, 17=17, 18=18, 19=19, 0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 20=20, 21=21, 22=22, 23=23, 24=24, 25=25, 26=26, 27=27, 28=28, 29=29, 30=30, 31=31, 32=32, 33=33, 34=34, 35=35, 36=36, 37=37, 38=38, 39=39, 40=40, 41=41, 42=42, 43=43, 44=44, 45=45, 46=46, 47=47, 48=48, 49=49, 50=50}
ConcurrentHashMap
在 JDK 1.7 及以前版本的运行原理:
在 JDK 1.7,ConcurrentHashMap
通过分段锁(Segment
,继承了 ReentrantLock
)的方式实现线程安全。它将一个大的 Map
分成了多个小的分段,每个分段都有自己的锁。不同的线程可以同时访问不同的分段锁,提高了并发性能。此外,ConcurrentHashMap
的读取操作不需要加锁,只有写入操作才需要加锁,也提高了并发性能。
使用空参构造器创建对象后,数组并未创建。第一次插入元素时,会创建一个长度为 16 的 Segment
数组(segments[]
,分段数组),这个数组长度固定不变,将整张哈希表分割成一个个的分段,写入数据时只会根据数据落入的分段而部分锁住这张哈希表,提高哈希表的并发性能。可以理解为 Segment
数组的长度就是这张哈希表所支持的最大并发数。
Segment
类中的字段存储了 count
(分段中元素数量)、modCount
(结构性修改次数)、threshold
(扩容阈值)、loadFactor
(加载因子)以及存储元素数据的数组 table
(HashEntry<K,V>
)。可以看到,元素真正要存储的位置是分段中的数组。
第一次插入元素,初始化 segments[]
后,该分段数组长度不可变,由构造器的参数 initialCapacity
决定(但最终会设置为最接近该值的 2 的幂,提高哈希效率)。但分段数组中的 table
长度可变。同时,segments[0]
的 table
会被初始化为一个长度为 2 的 HashEntry
数组,这由 Unsafe
完成。在真正插入元素前,分段数组初始化完毕,除了 segments[0]
,其余索引处分段均为 null
。
但是,数据仍未插入到表中。先进行第一次哈希计算求出元素的分段位置。若该分段为 null
,则会先按照模板创建 Segment
并初始化 table
;否则,则进行第二次哈希计算求得元素在 table
中的索引位置。如果需要扩容,则 table
被扩容为原来的两倍;若不需要扩容,则会判断该位置是否发生哈希冲突。若不发生哈希冲突,直接将元素存入 table
中;否则,调用 equals
方法判断元素是否一致。若元素一致,则放弃存储;否则,在 table
的该索引处形成一张链表。
最终,ConcurrentHashMap
会形成如下图所示的数据结构(数组 + 链表):
JDK 1.7 的 ConcurrentHashMap
虽然提高了并发性能,但由于插入元素时需要进行二次哈希计算,这相较于 HashMap
而言,速度相对较慢。因此,若不是在高并发的场景下,不要使用 ConcurrentHashMap
。
除此之外,由于读取操作并没有用锁保护,虽然保证合理同步的同时具有很高的效率,但是若往 ConcurrentHashMap
插入大量数据时(例如调用 putAll
),get
方法只能够获取成功插入的更新。
ConcurrentHashMap
在 JDK 1.8 及以后版本的运行原理:
类似于 HashMap
,若链表长度过长,遍历表的效率就会变得十分低下。因此,自 JDK 1.8 起,引入了红黑树的数据结构来解决之一问题。
同时,JDK 1.8 还对 ConcurrentHashMap
的数据结构做了调整,放弃了 Segment
分段锁的概念,转而使用 CAS + 监视器锁(synchronized
)的方式实现。因此,在 JDK 1.8 及以后,键值对不再由 Segment
中的 HashEntry
存储,转而使用 Node
存储,其中,key
和 value
都被声明为 volatile
,保证了可见性。
调用空参构造器,构造器什么也没做。第一次插入元素时,会先初始化哈希表,创建一个长度为 16 的数组。然后,计算元素在数组中的存储位置。若为空,则利用 CAS 算法插入到数组中,CAS 写入失败则自旋保证成功;否则,先判断是否需要扩容(由哈希值计算得出的存储位置判断是否需要扩容 hash == MOVED == -1
);若不需要扩容,这是说明发生哈希冲突,使用 synchronized
写入数据。
发生哈希冲突时,获取监视器锁写入数据,将元素挂在已有元素后形成链表(如果已经是红黑树,则挂在红黑树中)。写入完成后,再判断是否具备转换红黑树的条件。当数组长度大于等于 64 且 链表长度大于 8 时,链表将转化为红黑树。
5-10.2.3 CopyOnWrite
写入时复制
写入时复制是一种策略,通俗的理解是,当尝试往一个容器中添加元素时,不直接往容器中添加,而是先复制出新的容器,然后往新容器中添加元素。添加完元素后,再将容器的引用指向新容器。这样看来,这实现了读写分离,使得读操作可以并发进行而不需要加锁。
JUC 中提供了两个写入时复制的类 CopyOnWriteArrayList
和 CopyOnWriteArraySet
,而这实际上就是施加了写入时复制的列表和集合,且 CopyOnWriteArraySet
在内部维护了一个 CopyOnWriteArrayList
。
因此,我们只需要看看 CopyOnWriteArrayList
的线程安全实现方式。所有写入方法 add(Element)
, add(int, Element)
, addAll(Collection<? extends E>)
, addAll(int, Collection<? extends E>)
都在方法内部使用了监视器锁保护数据。代码实现比较简单。
由于写入操作需要复制,因此会增大时间和空间上的开销。CopyOnWriteArrayList
适用于读多写少的并发场景,如黑白名单、商品类目的访问和更新。
使用 CopyOnWriteArrayList
时,要注意:
- 减少扩容开销:根据实际需要,初始化
CopyOnWriteArrayList
的大小,避免写入时的扩容开销; - 使用批量添加:每次添加都会让容器进行复制,因此应尽可能地减少添加次数,可以减少容器复制次数;
5-10.3 CountDownLatch
闭锁
闭锁是一种同步辅助工具,允许一条或多条线程等待其他线程完成正在执行的操作。
闭锁会用一个所给的 count
参数初始化。await
方法会阻塞线程,直至其他线程调用 countDown
方法使得 count
最终减至 0 时,所有等待线程都会被释放,随后的所有 await
方法调用都会立即返回,不会阻塞。因此,这是一个一次性工具,count
不能够重置。若需要可重置计数器的版本,考虑使用 CyclicBarrier
。
闭锁是一个多用的同步工具,可用作多种不同用途。初始化 count
为 1 的闭锁可用作开关:所有调用了 await
的线程都会等待,直至该开关由另一线程调用 countDown
而打开。初始化 count
为 N
的闭锁可用在让一条线程等待 N 条线程执行完毕或某个任务执行 N 次。
构造方法:
构造方法 | 描述 |
---|---|
CountDownLatch(int count) |
初始化一个指定 count 的闭锁 |
成员方法:
方法 | 描述 |
---|---|
void await() |
让当前线程等待,直至闭锁计数器降为 0,或线程被中断 |
boolean await(long timeout, TimeUnit unit) |
让当前线程等待,直至闭锁计数器降为 0,或线程被中断,或超时 |
void countDown() |
让闭锁计数器自减,计数器归零时释放所有的等待线程 |
long getCount() |
返回闭锁计数器的值 |
一个简单的示例:
package com.multithreading.utils;
import java.util.concurrent.CountDownLatch;
public class Consumer implements Runnable {
private CountDownLatch latch;
public Consumer(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
// 消费
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":正在吃第 " + (i + 1) + " 个饺子。");
}
// 消费完毕
latch.countDown();
}
}
package com.multithreading.utils;
import java.util.concurrent.CountDownLatch;
public class Cleaner implements Runnable {
private CountDownLatch latch;
public Cleaner(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
// 收拾碗筷
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":正在清理碗筷。");
}
}
package com.multithreading.utils;
import java.util.concurrent.CountDownLatch;
public class LatchDemo {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(3);
Thread cleaner = new Thread(new Cleaner(latch), "清洁人员");
Thread consumerA = new Thread(new Consumer(latch), "消费者A");
Thread consumerB = new Thread(new Consumer(latch), "消费者B");
Thread consumerC = new Thread(new Consumer(latch), "消费者C");
cleaner.start();
consumerA.start();
consumerB.start();
consumerC.start();
}
}
5-10.4 Semaphore
信号量
一个计数的信号量。概念上来说,一个信号量维护一组许可。每一个 acquire()
都会在必要时阻塞至有可用许可为止,然后获取许可。每一个 release()
都会添加许可,可能释放一个等待中的线程。但是实际上,并没有使用具体的许可对象;信号量只是维护可用数量并根据情况做出行动。
信号量通常用于限制访问某些数据(物理或逻辑数据)的线程数。和锁十分类似,其方法实现的语义也十分类似于锁,但不同于 ReentrantLock
,信号量允许多条线程同时访问资源。
构造方法:
构造方法 | 描述 |
---|---|
Semaphore(int permits) |
创建一个具有指定数量许可的非公平信号量 |
Semaphore(int permits, boolean fair) |
出啊构建一个具有指定数量许可、指定公平性的信号量 |
成员方法:
方法 | 描述 |
---|---|
void acquire() |
从信号量中获取一个许可,会阻塞至具有一个可用许可为止,或阻塞至线程被中断为止 |
void acquire(int permits) |
从信号量中获取一定数量许可,阻塞至具有足够可用许可为止,或阻塞至线程被中断为止 |
void acquireUninterruptibly() |
不可中断地从信号量中获取许可,阻塞至具有一个可用许可为止,获取时间可能发生变化 |
void acquireUninterruptibly(int permits) |
不可中断地从信号量中获取一定数量许可,阻塞至具有足够可用许可为止,获取时间可能发生变化 |
int availablePermits() |
返回信号量当前可用的许可数目 |
final int getQueuedLength() |
返回信号量中等待中线程的估计数目 |
final boolean hasQueuedThreads() |
查询是否有线程正在等待获取许可 |
boolean isFair() |
查询信号量是否公平 |
void release() |
释放许可,将许可归还至信号量 |
void release(int permits) |
释放一定数量许可,并归还至信号量 |
boolean tryAcquire() |
尝试从信号量中获取一个许可,仅尝试一次,若有可用许可立即取得,这会破坏公平性 |
boolean tryAcquire(long timeout, TimeUnit unit) |
尝试从信号量中获取一个许可,仅尝试一次,若在限时内有可用则立即取得,这会破坏公平性 |
boolean tryAcquire(int permits) |
尝试从信号量中获取一定数量许可,仅尝试一次,若全部可用则立即取得,这会破坏公平性 |
boolean tryAcquire(int permits, long timeout, TimeUnit unit) |
尝试从信号量中获取一定数量许可,仅尝试一次,若在限时内全部可用则立即取得,这会破坏公平性 |
使用步骤:
- 需要由管理员管理信号量通道;
- 当有 ”车“ 来了,发放许可证;
- 当有 ”车“ 出去了,收回许可证;
- 若通行证发完了,其他 ”车辆“ 只能够等待;
一个简单的示例:
package com.multithreading.utils;
import java.util.conurrent.Semaphore;
public class ParkingLot implements Runnable {
// 五个停车位
Semaphore semaphore = new Semaphore(5);
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":已驶入停车场,余位 " + semaphore.availablePermits());
semaphore.release();
System.out.println(Thread.currentThread().getName() + ":以驶出停车场,余位 " + semaphore.availablePermits());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
package com.multithreading.utils;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
ParkingLot parkingLot = new ParkingLot();
for (int i = 0; i < 100; i++) {
new Thread(parkingLot).start();
}
}
}
5-10.5 CyclicBarrier
循环栅栏
一个同步辅助工具,允许一组线程相互等待,直到到达一个共同栅栏点。在涉及到固定数目线程的应用程序中,循环栅栏非常有用,这些线程必须偶尔相互等待。该类命名为循环栅栏,是因为它相较于闭锁而言,在所有线程被释放时仍能够继续使用。
也就是说,线程调用 await
方法后,线程就会来到栅栏旁等待。多条线程调用 await
可以实现多条线程都在栅栏旁相互等待。一旦有 N 条线程到达栅栏,所有线程都会被释放,继续运行,称为越过栅栏。栅栏点就是满足线程释放时,在栅栏处等待的线程数。
循环栅栏支持可选的 Runnable
栅栏行动,栅栏行动在每次到达栅栏点时执行一次。该栅栏行动在所有线程都到达栅栏,在越过栅栏前由最后一条到达栅栏的线程执行一次。栅栏行动在所有线程继续执行前更新共享状态时很实用。
另外,JDK 7 还引入了一个新的并发 API,Phaser
(移相器),功能与 CountDownLatch
和 CyclicBarrier
类似,但使用更灵活。
构造方法:
构造方法 | 描述 |
---|---|
CyclicBarrier(int parties) |
创建一个循环栅栏,当有指定数量线程等待时越过栅栏,不执行任何预定义的栅栏行动 |
CyclicBarrier(int parties, Runnable barrierAction) |
创建一个循环栅栏,当有指定数量线程等待时越过栅栏,会在越过栅栏时执行栅栏行动 |
成员方法:
方法 | 描述 |
---|---|
int await() |
等待,直到所有线程都调用了 await 在栅栏旁等待时被释放 |
int await(long timeout, TimeUnit unit) |
等待,直到所有线程都调用了 await 在栅栏旁等待时,或超时被释放 |
int getNumberWaiting() |
返回当前正在栅栏旁等待的线程数目 |
int getParties() |
返回越过栅栏所需的线程数目 |
boolean isBroken() |
查询栅栏是否已损坏 |
void reset() |
重置栅栏,让栅栏回到初始状态 |
注意:
await()
,await(long, TimeUnit)
方法:只要有足够的线程同时在栅栏旁等待,就会先执行栅栏行动(如果有的话),然后一起释放线程,释放是自动进行的,直到下一次需要再次释放线程时,重复上述操作,以此往复;isBroken()
方法:若有线程从栅栏创建起或上次重置后,由于等待时被中断而抛出InterruptedException
冲出栅栏,栅栏就被损坏;或者栅栏行动执行过程中抛出异常导致栅栏损坏,方法返回true
。该方法可用于测试线程或栅栏行动是否抛出异常;reset()
方法:若调用时有线程正在栅栏旁等待,线程会返回,抛出BrokenBarrierException
。且注意,出于某些原因栅栏损坏后调用reset
会导致重置较难执行;线程需要以某种方式重新同步,并选择一条线程执行重置。相反,更可取的办法是创建一个新栅栏以供后续使用;
一个简单的示例:
package com.multithreading.utils;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
// 线程执行的任务
public class Task implements Runnable {
private CyclicBarrier barrier;
public Task(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ":正在栅栏处等待。");
barrier.await();
System.out.println(Thread.currentThread().getName() + ":完成。");
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
package com.multithreading.utils;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
Runnable barrierAction = () -> System.out.println(Thread.currentThread().getName() + ":已越过栅栏。");
CyclicBarrier barrier = new CyclicBarrier(3, barrierAction);
Task task = new Task(barrier);
Thread workerA = new Thread(task, "工作者A");
Thread workerB = new Thread(task, "工作者B");
Thread workerC = new Thread(task, "工作者C");
workerA.start();
workerB.start();
workerC.start();
}
}
运行后,三条线程会依次在栅栏处等待,最后一条到达栅栏的线程会执行栅栏行动,然后一同越过栅栏,线程继续执行接下来的任务。
5-10.6 Exchanger
交换器
交换器表示线程进行配对和交换任务的同步点。每条线程在执行 exchange()
方法时会先递交某个对象,然后与一条伙伴线程配对,最后获取伙伴线程的对象。交换器可视作一种双向形式的 SynchronousQueue
(不存储任何元素的阻塞队列,每一个插入操作必须要有对应的删除操作,反之亦然)。交换器在使用遗传算法和流水线设计的应用程序中十分有用。
构造方法:
构造方法 | 描述 |
---|---|
Exchanger() |
构造一个新的交换器 |
成员方法:
方法 | 描述 |
---|---|
V exchange(V x) |
等待另一条线程到达该交换点(除非当前线程被中断),然后将所给对象传输给对方,并接收对方的对象 |
V exhcange(V x, long timeout, TinmeUnit unit) |
等待另一条线程到达该交换点(除非当前线程被中断或超时),然后将所给对象传输给对方,并接收对方的对象 |
示例:
package com.multithreading.utils;
import java.util.concurrent.Exchanger;
// 线程执行的任务
public class ExchangeTask implements Runnable {
private final Exchanger exchanger;
Object object;
public ExchangeTask(Exchanger exchanger, Object object) {
this.exchanger = exchanger;
this.object = object;
}
@Override
public void run() {
try {
final Object prev = this.object;
this.object = exchanger.exchange(prev);
System.out.println(Thread.currentThread().getName() + ":成功将 " + prev.toString() + " 交换为 " + this.object.toString());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
package com.multithreading.utils;
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<Integer> exchanger = new Exchanger<>();
ExchangeTask holderA = new ExchangeTask(exchanger, 10);
ExchangeTask holderB = new ExchangeTask(exchanger, 20);
System.out.println(holderA);
System.out.println(holderB);
Thread exchangerA = new Thread(holderA, "线程A");
Thread exchangerB = new Thread(holderB, "线程B");
exchangerA.start();
exchangerB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(holderA);
System.out.println(holderB);
}
}
两条线程启动后,成功交换数据。
5-10.7 参考
一文读懂Java ConcurrentHashMap原理与实现 - 知乎 (zhihu.com)
Java并发编程笔记之ConcurrentHashMap原理探究 - 国见比吕 - 博客园 (cnblogs.com)
ConcurrentHashMap 源码分析 | JavaGuide(Java面试 + 学习指南)