JUC并发工具包使用指南
- 序言
- 一、锁与原子类型
- 二、阻塞队列 BlockingQueue
- 三、线程与线程池
- 四、并发容器
- 五、线程执行控制辅助类
序言
本文只要是对java.util.concurrent包下的相关开发工具做一个简单的介绍并尝试在项目中使用它,本文不会去解释关于 Java 并发的核心问题 其背后的原理,也就是说,如果对那些东西感兴趣,请参考《Java 并发指南》。
一、锁与原子类型
java.util.concurrent.locks.Lock 是一个类似于 synchronized 块的线程同步机制。但是 Lock 比 synchronized 块更加灵活、精细。
1、可重用锁 Lock
ava.util.concurrent.locks 包提供了以下对 Lock 接口的实现类:ReentrantLock
1.2、Lock、synchronized比较
一个 Lock 对象和一个 synchronized 代码块之间的主要不同点是:
- synchronized 代码块不能够保证进入访问等待的线程的先后顺序。
- 不能够传递任何参数给一个 synchronized 代码块的入口。因此,对于 synchronized 代码块的访问等待设置超时时间是不可能的事情。
- synchronized 块必须被完整地包含在单个方法里。而一个 Lock 对象可以把它的 lock() 和 unlock() 方法的调用放在不同的方法里。
1.3 、Lock 的方法
Lock 接口具有以下主要方法:
- lock()
lock() 将 Lock 实例锁定。如果该 Lock 实例已被锁定,调用 lock() 方法的线程将会阻塞,直到 Lock 实例解锁。 - lockInterruptibly()
lockInterruptibly() 方法将会被调用线程锁定,除非该线程被打断。此外,如果一个线程在通过这个方法来锁定 Lock 对象时进入阻塞等待,而它被打断了的话,该线程将会退出这个方法调用。 - tryLock()
tryLock() 方法试图立即锁定 Lock 实例。如果锁定成功,它将返回 true,如果 Lock 实例已被锁定该方法返回 false。这一方法永不阻塞。 - tryLock(long timeout, TimeUnit timeUnit)
tryLock(long timeout, TimeUnit timeUnit) 的工作类似于 tryLock() 方法,除了它在放弃锁定 Lock 之前等待一个给定的超时时间之外。 - unlock()
unlock() 方法对 Lock 实例解锁。一个 Lock 实现将只允许锁定了该对象的线程来调用此方法。其他(没有锁定该 Lock 对象的线程)线程对 unlock() 方法的调用将会抛一个未检查异常(RuntimeException)。
1.4、锁的例子
package com.juc.demo.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockDemo implements Runnable{
/**
* 可重用锁
*/
private Lock lock = new ReentrantLock();
@Override
public void run() {
test();
}
private void test() {
lock.lock();
try{
//同步代码块
}finally {
lock.unlock();
}
}
}
1.5、Condition精确通知
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
Condition的使用方式比较简单,需要注意在调用方法前获取锁,如下代码所示:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
如下有一个精确通知的实例,该例子想要让第一个线程先执行,然后能够准确的通知第二个阻塞的线程执行,再精确的通知第三个阻塞的线程执行。代码如下实例所示:
package com.test;
import com.utils.ThreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
private static Lock lock = new ReentrantLock();
/**
* 信号量
*/
private static Integer number = 0;
private static Condition condition1 = lock.newCondition();
private static Condition condition2 = lock.newCondition();
private static Condition condition3 = lock.newCondition();
public static void main(String[] args) {
//使用自定义线程池,该线程池在第三章3点3.2中
ThreadPool instance = ThreadPool.getInstance();
ExecutorService executorService = instance.getExecutorService();
executorService.execute(() -> {
lock.lock();
try {
while (number != 0) {
condition1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "打印1");
}
number = 1;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
executorService.execute(() -> {
lock.lock();
try {
while (number != 1) {
condition2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "打印2");
}
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
executorService.execute(() -> {
lock.lock();
try {
while (number != 2) {
condition3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "打印3");
}
number = 0;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
}
}
2、读写锁 ReadWriteLock
java.util.concurrent.locks.ReadWriteLock 读写锁是一种先进的线程锁机制。它能够允许多个线程在同一时间对某特定资源进行读取,但同一时间内只能有一个线程对其进行写入。读写锁的理念在于多个线程能够对一个共享资源进行读取,而不会导致并发问题。并发问题的发生场景在于对一个共享资源的读和写操作的同时进行,或者多个写操作并发进行。
2.1、ReadWriteLock 锁规则
一个线程在对受保护资源在读或者写之前对 ReadWriteLock 锁定的规则如下:
- 读锁:如果没有任何写操作线程锁定 ReadWriteLock,并且没有任何写操作线程要求一个写锁(但还没有获得该锁)。因此,可以有多个读操作线程对该锁进行锁定。
- 写锁:如果没有任何读操作或者写操作。因此,在写操作的时候,只能有一个线程对该锁进行锁定。
简而言之:
- 读读能共存
- 读写不能共存
- 写写不能共存
2.2、ReadWriteLock 实现
ReadWriteLock 是个接口,如果想用它的话就得去使用它的实现类之一。java.util.concurrent.locks 包提供了 ReadWriteLock 接口的以下实现类:ReentrantReadWriteLock
2.3、ReadWriteLock 代码示例
package com.juc.demo.lock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
* 但是如果有一个线程象取写共享资源来,就不应该自由其他线程可以对资源进行读或写
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 0; i < 5; i++) {
final int tmp = i;
new Thread(()->{
myCache.put(tmp+"",tmp+"");
},"Thread"+i).start();
}
for (int i = 0; i < 5; i++) {
final int tmp = i;
new Thread(()->{
myCache.get(tmp+"");
},"Thread"+i).start();
}
}
}
class MyCache {
private volatile Map<String,Object> map = new HashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key);
TimeUnit.MILLISECONDS.sleep(300);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t写入完成");
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t正在读取:" + key);
TimeUnit.MILLISECONDS.sleep(3000);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t读取完成" + o);
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
public void clear(){
map.clear();}
}
注意如何使用 ReadWriteLock 对两种锁实例的持有。一个对读访问进行保护,一个对写访问进行保护。
3、原子性类型
3.1、AtomicBoolean
AtomicBoolean 类为我们提供了一个可以用原子方式进行读和写的布尔值,它还拥有一些先进的原子性操作,比如 compareAndSet()。AtomicBoolean 类位于 java.util.concurrent.atomic 包,完整类名是为 java.util.concurrent.atomic.AtomicBoolean。本小节描述的 AtomicBoolean 是 Java 8 版本里的,而不是它第一次被引入的 Java 5 版本。
(1)创建一个AtomicBoolean
AtomicBoolean atomicBoolean = new AtomicBoolean();
以上示例新建了一个默认值为 false 的 AtomicBoolean。
如果想要为 AtomicBoolean 实例设置一个显式的初始值,那么可以将初始值传给 AtomicBoolean 的构造方法:
AtomicBoolean atomicBoolean = new AtomicBoolean(true)
(2)获取 AtomicBoolean 的值
可以通过使用 get() 方法来获取一个 AtomicBoolean 的值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean value = atomicBoolean.get();
以上代码执行后 value 变量的值将为 true。
(3)设置 AtomicBoolean 的值
可以通过使用 set() 方法来设置一个 AtomicBoolean 的值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
atomicBoolean.set(false);
以上代码执行后 AtomicBoolean 的值为 false。
(4) 交换 AtomicBoolean 的值
可以通过 getAndSet() 方法来交换一个 AtomicBoolean 实例的值。getAndSet() 方法将返回 AtomicBoolean 当前的值,并将为 AtomicBoolean 设置一个新值。示例如下:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean oldValue = atomicBoolean.getAndSet(false);
以上代码执行后 oldValue 变量的值为 true,atomicBoolean 实例将持有 false 值。代码成功将 AtomicBoolean 当前值 ture 交换为 false。
(5) 比较并设置 AtomicBoolean 的值
compareAndSet() 方法允许对 AtomicBoolean 的当前值与一个期望值进行比较,如果当前值等于期望值的话,将会对 AtomicBoolean 设定一个新值。compareAndSet() 方法是原子性的,因此在同一时间之内有单个线程执行它。因此 compareAndSet() 方法可被用于一些类似于锁的同步的简单实现。
以下是一个 compareAndSet() 示例:
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean expectedValue = true;
boolean newValue = false;
boolean wasNewValueSet = atomicBoolean.compareAndSet(expectedValue, newValue);
本示例对 AtomicBoolean 的当前值与 true 值进行比较,如果相等,将 AtomicBoolean 的值更新为 false。
3.2、原子性整型 AtomicInteger
AtomicInteger 类为我们提供了一个可以进行原子性读和写操作的 int 变量,它还包含一系列先进的原子性操作,比如 compareAndSet()。AtomicInteger 类位于 java.util.concurrent.atomic 包,因此其完整类名为 java.util.concurrent.atomic.AtomicInteger。本小节描述的 AtomicInteger 是 Java 8 版本里的,而不是它第一次被引入的 Java 5 版本。
(1)创建一个 AtomicInteger
创建一个 AtomicInteger 示例如下:
AtomicInteger atomicInteger = new AtomicInteger();
本示例将创建一个初始值为 0 的 AtomicInteger。
如果想要创建一个给定初始值的 AtomicInteger,可以这样:
AtomicInteger atomicInteger = new AtomicInteger(123);
本示例将 123 作为参数传给 AtomicInteger 的构造子,它将设置 AtomicInteger 实例的初始值为 123。
(2) 获取 AtomicInteger 的值
可以使用 get() 方法获取 AtomicInteger 实例的值。示例如下:
AtomicInteger atomicInteger = new AtomicInteger(123);
int theValue = atomicInteger.get();
(3) 设置 AtomicInteger 的值
可以通过 set() 方法对 AtomicInteger 的值进行重新设置。以下是 AtomicInteger.set() 示例:
AtomicInteger atomicInteger = new AtomicInteger(123);
atomicInteger.set(234);
以上示例创建了一个初始值为 123 的 AtomicInteger,而在第二行将其值更新为 234。
(4) 比较并设置 AtomicInteger 的值
AtomicInteger 类也通过了一个原子性的 compareAndSet() 方法。这一方法将 AtomicInteger 实例的当前值与期望值进行比较,如果二者相等,为 AtomicInteger 实例设置一个新值。
AtomicInteger.compareAndSet() 代码示例:
AtomicInteger atomicInteger = new AtomicInteger(123);
int expectedValue = 123;
int newValue = 234;
atomicInteger.compareAndSet(expectedValue, newValue);
本示例首先新建一个初始值为 123 的 AtomicInteger 实例。然后将 AtomicInteger 与期望值 123 进行比较,如果相等,将 AtomicInteger 的值更新为 234。
(5)增加 AtomicInteger 值
AtomicInteger 类包含有一些方法,通过它们可以增加 AtomicInteger 的值,并获取其值。这些方法如下:
- addAndGet()
addAndGet() 方法给 AtomicInteger 增加了一个值,然后返回增加后的值。以下是这两种方法的示例:
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.addAndGet(10));
本示例将打印出 10。例子中第二行将 AtomicInteger 的值加 10,并返回加操作之后的值。该值现在是为 10。
- getAndAdd()
getAndAdd() 方法为 AtomicInteger 增加了一个值,但返回的是增加以前的 AtomicInteger 的值。具体使用哪一个取决于应用场景。以下是这种方法的示例:
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.getAndAdd(10));
当然也可以使用这俩方法为 AtomicInteger 添加负值。结果实际是一个减法操作。
- getAndIncrement()
- incrementAndGet()
getAndIncrement() 和 incrementAndGet() 方法类似于 getAndAdd() 和 addAndGet(),但每次只将 AtomicInteger 的值加 1。
(6) 减小 AtomicInteger 的值
AtomicInteger 类还提供了一些减小 AtomicInteger 的值的原子性方法。这些方法是:
- decrementAndGet()
- getAndDecrement()
decrementAndGet() 将 AtomicInteger 的值减一,并返回减一后的值。getAndDecrement() 也将 AtomicInteger 的值减一,但它返回的是减一之前的值。
3.3、原子性长整型 AtomicLong
AtomicLong 类为我们提供了一个可以进行原子性读和写操作的 long 变量,它还包含一系列先进的原子性操作,比如 compareAndSet()AtomicLong 类位于 java.util.concurrent.atomic 包,因此其完整类名为 java.util.concurrent.atomic.AtomicLong。本小节描述的 AtomicLong 是 Java 8 版本里的,而不是它第一次被引入的 Java 5 版本。由于该类型的使用与API与AtomicInteger基本相似,此处不再赘述。
3.4、原子性引用型 AtomicReference
AtomicReference 提供了一个可以被原子性读和写的对象引用变量。原子性的意思是多个想要改变同一个 AtomicReference 的线程不会导致 AtomicReference 处于不一致的状态。AtomicReference 还有一个 compareAndSet() 方法,通过它可以将当前引用于一个期望值(引用)进行比较,如果相等,在该 AtomicReference 对象内部设置一个新的引用。
(1) 创建一个 AtomicReference
创建 AtomicReference 如下:
AtomicReference atomicReference = new AtomicReference();
如果需要使用一个指定引用创建 AtomicReference,可以:
String initialReference = "the initially referenced string";
AtomicReference atomicReference = new AtomicReference(initialReference);
(2)创建泛型 AtomicReference
可以使用 Java 泛型来创建一个泛型 AtomicReference。示例:
AtomicReference<String> atomicStringReference = new AtomicReference<String>();
也可以为泛型 AtomicReference 设置一个初始值。示例:
String initialReference = "the initially referenced string";
AtomicReference<String> atomicStringReference = new AtomicReference<String>(initialReference);
(3) 获取 AtomicReference 引用
可以通过 AtomicReference 的 get() 方法来获取保存在 AtomicReference 里的引用。如果AtomicReference 是非泛型的,get() 方法将返回一个 Object 类型的引用。如果是泛型化的,get() 将返回创建 AtomicReference 时声明的那个类型。
先来看一个非泛型的 AtomicReference get() 示例:
AtomicReference atomicReference = new AtomicReference("first value referenced");
String reference = (String) atomicReference.get();
注意如何对 get() 方法返回的引用强制转换为 String。
泛型化的 AtomicReference 示例:
AtomicReference<String> atomicReference = new AtomicReference<String>("first value referenced");
String reference = atomicReference.get();
编译器知道了引用的类型,所以我们无需再对 get() 返回的引用进行强制转换了。
(4)设置 AtomicReference 引用
可以使用 get() 方法对 AtomicReference 里边保存的引用进行设置。如果定义的是一个非泛型 AtomicReference,set() 将会以一个 Object 引用作为参数。如果是泛型化的 AtomicReference,set() 方法将只接受定义给的类型。
AtomicReference set() 示例:
AtomicReference atomicReference = new AtomicReference();
atomicReference.set("New object referenced");
这个看起来非泛型和泛型化的没啥区别。真正的区别在于编译器将对能够设置给一个泛型化的 AtomicReference 参数类型进行限制。
(5) 比较并设置 AtomicReference 引用
AtomicReference 类具备了一个很有用的方法:compareAndSet()。compareAndSet() 可以将保存在 AtomicReference 里的引用于一个期望引用进行比较,如果两个引用是一样的(并非 equals() 的相等,而是 == 的一样),将会给AtomicReference 实例设置一个新的引用。
如果 compareAndSet() 为 AtomicReference 设置了一个新的引用,compareAndSet() 将返回 true。否则compareAndSet() 返回 false。
AtomicReference compareAndSet() 示例:
String initialReference = "initial value referenced";
AtomicReference<String> atomicStringReference = new AtomicReference<String>(initialReference);
String newReference = "new value referenced";
boolean exchanged = atomicStringReference.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);
exchanged = atomicStringReference.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged); 1234567891011
本示例创建了一个带有一个初始引用的泛型化的 AtomicReference。之后两次调用 comparesAndSet()来对存储值和期望值进行对比,如果二者一致,为 AtomicReference 设置一个新的引用。第一次比较,存储的引用(initialReference)和期望的引用(initialReference)一致,所以一个新的引用(newReference)被设置给 AtomicReference,compareAndSet() 方法返回 true。第二次比较时,存储的引用(newReference)和期望的引用(initialReference)不一致,因此新的引用没有被设置给 AtomicReference,compareAndSet() 方法返回 false。
二、阻塞队列 BlockingQueue
java.util.concurrent 包里的 BlockingQueue 接口表示一个线程放入和提取实例的队列。本小节我将演示如何使用这个 BlockingQueue。
1、BlockingQueue 基本介绍
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是对这个原理的阐述:
一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。
负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
2、BlockingQueue的方法
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
操作 | 抛异常 | 特定值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(o) | offer(o) | put(o) | offer(o, timeout, timeunit) |
移除 | remove(o) | poll(o) | take(o) | poll(timeout, timeunit) |
检查 | element(o) | peek(o) | 不可用 | 不可用 |
四组不同的行为方式解释:
- 抛异常:如果尝试的操作无法立即执行,抛一个异常。
- 特定值:如果尝试的操作无法立即执行,返回一个特定的值(常常是 true / false)。
- 阻塞:如果尝试的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
- 超时:如果尝试的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
无法向一个 BlockingQueue 中插入 null。如果试图插入 null,BlockingQueue 将会抛出一个 NullPointerException。
可以访问到 BlockingQueue 中的所有元素,而不仅仅是开始和结束的元素。比如说,将一个对象放入队列之中以等待处理,但的应用想要将其取消掉。那么可以调用诸如 remove(o) 方法来将队列之中的特定对象进行移除。但是这么干效率并不高(译者注:基于队列的数据结构,获取除开始或结束位置的其他对象的效率不会太高),因此尽量不要用这一类的方法,除非确实不得不那么做。
3、BlockingQueue的实现
BlockingQueue 是个接口,需要使用它的实现之一来使用 BlockingQueue。java.util.concurrent 具有以下 BlockingQueue 接口的实现(Java 6):
- ArrayBlockingQueue
- DelayQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
- SynchronousQueue
4、数组阻塞队列ArrayBlockingQueue
ArrayBlockingQueue 类实现了 BlockingQueue 接口。
ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
ArrayBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个。
以下是在使用 ArrayBlockingQueue 的时候对其初始化的一个示例:
BlockingQueue queue = new ArrayBlockingQueue(1024);
queue.put("1");
Object object = queue.take();
以下是使用了 Java 泛型的一个 BlockingQueue 示例。注意其中是如何对 String 元素放入和提取的:
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(1024);
queue.put("1");
String string = queue.take();
5、延迟队列DelayQueue
DelayQueue 实现了 BlockingQueue 接口。
DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口,该接口定义:
public interface Delayed extends Comparable<Delayed> {
public long getDelay(TimeUnit timeUnit);
}
DelayQueue 将会在每个元素的 getDelay() 方法返回的值的时间段之后才释放掉该元素。如果返回的是 0 或者负值,延迟将被认为过期,该元素将会在 DelayQueue 的下一次 take 被调用的时候被释放掉。
传递给 getDelay 方法的 getDelay 实例是一个枚举类型,它表明了将要延迟的时间段。TimeUnit 枚举将会取以下值:
- DAYS
- HOURS
- MINUTES
- SECONDS
- MILLISECONDS
- MICROSECONDS
- NANOSECONDS
正如所看到的,Delayed 接口也继承了 java.lang.Comparable 接口,这也就意味着 Delayed 对象之间可以进行对比。这个可能在对 DelayQueue 队列中的元素进行排序时有用,因此它们可以根据过期时间进行有序释放。
以下是使用 DelayQueue 的例子:
public class DelayQueueExample {
public static void main(String[] args) {
DelayQueue queue = new DelayQueue();
Delayed element1 = new DelayedElement();
queue.put(element1);
Delayed element2 = queue.take();
}
}
DelayedElement 是我所创建的一个 DelayedElement 接口的实现类,它不在 Java.util.concurrent 包里。需要自行创建自己的 Delayed 接口的实现以使用 DelayQueue 类。
6、链阻塞队列LinkedBlockingQueue
LinkedBlockingQueue 类实现了 BlockingQueue 接口。
LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
LinkedBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个。
以下是 LinkedBlockingQueue 的初始化和使用示例代码:
BlockingQueue<String> unbounded = new LinkedBlockingQueue<String>();
BlockingQueue<String> bounded = new LinkedBlockingQueue<String>(1024);
bounded.put("Value");
String value = bounded.take();
7、具有优先级的阻塞队列PriorityBlockingQueue
PriorityBlockingQueue 类实现了 BlockingQueue 接口。
PriorityBlockingQueue 是一个无界的并发队列。它使用了和类java.util.PriorityQueue 一样的排序规则。无法向这个队列中插入 null 值。
所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于自己的 Comparable 实现。
注意 PriorityBlockingQueue 对于具有相等优先级(compare() == 0)的元素并不强制任何特定行为。
同时注意,如果从一个 PriorityBlockingQueue 获得一个 Iterator 的话,该 Iterator 并不能保证它对元素的遍历是以优先级为序的。
以下是使用 PriorityBlockingQueue 的示例:
BlockingQueue queue = new PriorityBlockingQueue();
//String implements java.lang.Comparable
queue.put("Value");
String value = queue.take();
8、同步队列SynchronousQueue
SynchronousQueue 类实现了 BlockingQueue 接口。
SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
9、阻塞双端队列BlockingDeque
java.util.concurrent 包里的 BlockingDeque 接口表示一个线程安放入和提取实例的双端队列。本小节我将演示如何使用 BlockingDeque。
BlockingDeque 类是一个双端队列,在不能够插入元素时,它将阻塞住试图插入元素的线程;在不能够抽取元素时,它将阻塞住试图抽取的线程。
deque(双端队列) 是 “Double Ended Queue” 的缩写。因此,双端队列是一个可以从任意一端插入或者抽取元素的队列。
9.1、BlockingDeque的使用
在线程既是一个队列的生产者又是这个队列的消费者的时候可以使用到 BlockingDeque。如果生产者线程需要在队列的两端都可以插入数据,消费者线程需要在队列的两端都可以移除数据,这个时候也可以使用 BlockingDeque。BlockingDeque 图解:
一个 BlockingDeque - 线程在双端队列的两端都可以插入和提取元素。
一个线程生产元素,并把它们插入到队列的任意一端。如果双端队列已满,插入线程将被阻塞,直到一个移除线程从该队列中移出了一个元素。如果双端队列为空,移除线程将被阻塞,直到一个插入线程向该队列插入了一个新元素。
9.2 BlockingDeque的方法
BlockingDeque 具有 4 组不同的方法用于插入、移除以及对双端队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
操作 | 抛异常 | 特定值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | addFirst(o) | offerFirst(o) | putFirst(o) | offerFirst(o, timeout, timeunit) |
移除 | removeFirst(o) | pollFirst(o) | takeFirst(o) | pollFirst(timeout, timeunit) |
检查 | getFirst(o) | peekFirst(o) | 不可用 | 不可用 |
方法First结尾是指从队尾操作,与之对应的,讲First替换成Last,便是在队尾操作。
四组不同的行为方式解释:
- 抛异常:如果试图的操作无法立即执行,抛一个异常。
- 特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
- 阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
- 超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
9.3、BlockingDeque 继承自BlockingQueue
BlockingDeque 接口继承自 BlockingQueue 接口。这就意味着可以像使用一个 BlockingQueue 那样使用BlockingDeque。如果这么干的话,各种插入方法将会把新元素添加到双端队列的尾端,而移除方法将会把双端队列的首端的元素移除。正如 BlockingQueue 接口的插入和移除方法一样。
以下是 BlockingDeque 对 BlockingQueue 接口的方法的具体内部实现:
BlockingQueue | BlockingDeque |
---|---|
add() | addLast() |
offer() | offerLast() |
put() | putLast() |
offer(e, time, unit) | offerLast(e, time, unit) |
remove() | removeFirst() |
poll() | pollFirst() |
take() | takeFirst() |
poll(time, unit) | pollLast(time, unit) |
element() | getFirst() |
peek() | peekFirst() |
9.4、BlockingDeque 的实现
既然 BlockingDeque 是一个接口,那么想要使用它的话就得使用它的众多的实现类的其中一个。java.util.concurrent 包提供了以下 BlockingDeque 接口的实现类:LinkedBlockingDeque
9.5、BlockingDeque 代码示例
以下是如何使用 BlockingDeque 方法的一个简短代码示例:
BlockingDeque<String> deque = new LinkedBlockingDeque<String>();
deque.addFirst("1");
deque.addLast("2");
String two = deque.takeLast();
String one = deque.takeFirst();
10、链阻塞双端队列 LinkedBlockingDeque
LinkedBlockingDeque 类实现了 BlockingDeque 接口。
deque(双端队列) 是 “Double Ended Queue” 的缩写。因此,双端队列是一个可以从任意一端插入或者抽取元素的队列。
LinkedBlockingDeque 是一个双端队列,在它为空的时候,一个试图从中抽取数据的线程将会阻塞,无论该线程是试图从哪一端抽取数据。
以下是 LinkedBlockingDeque 实例化以及使用的示例:
BlockingDeque<String> deque = new LinkedBlockingDeque<String>();
deque.addFirst("1");
deque.addLast("2");
String two = deque.takeLast();
String one = deque.takeFirst();
三、线程与线程池
java.util.concurrent.ExecutorService 接口为一个异步执行机制,使我们能够在后台执行任务。因此一个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的 ExecutorService 实现就是一个线程池实现。
1、创建一个 ExecutorService
ExecutorService 的创建依赖于使用的具体实现。但是也可以使用 Executors 工厂类来创建 ExecutorService 实例。以下是几个创建 ExecutorService 实例的例子:
//仅有一个线程的线程池
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
//固定线程数量的线程池
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
//支持定时以及周期性执行任务
ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
//缓存线程池,使用量增多线程池也会增大
Executors.newCachedThreadPool();
2、ExecutorService 使用
有几种不同的方式来将任务委托给 ExecutorService 去执行:
2.1、execute(Runnable)
execute(Runnable) 方法要求一个 java.lang.Runnable 对象,然后对它进行异步执行。以下是使用 ExecutorService 执行一个 Runnable 的示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
executorService.shutdown();
没有办法得知被执行的 Runnable 的执行结果。如果有需要的话得使用一个 Callable(以下将做介绍)。
2.2、submit(Runnable)
submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个 Future 对象可以用来检查 Runnable 是否已经执行完毕。
以下是 ExecutorService submit() 示例:
Future future = executorService.submit(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
future.get(); //returns null if the task has finished correctly.
2.3、submit(Callable)
submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。Runnable.run() 不能够返回一个结果。
Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。以下是一个 ExecutorService Callable 示例:
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
});
System.out.println("future.get() = " + future.get());
以上代码输出:
Asynchronous Callable
future.get() = Callable Result
2.4、invokeAny(…)
invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个Future,但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果只能表明其中一个已执行结束。如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。以下是示例代码:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();
上述代码将会打印出给定 Callable 集合中的一个的执行结果。我自己试着执行了它几次,结果始终在变。有时是 “Task 1”,有时是 “Task 2” 等等。
2.5、invokeAll(…)
invokeAll() 方法将调用集合中传给 ExecutorService 的所有 Callable 对象。invokeAll() 返回一系列的 Future 对象,通过它们可以获取每个 Callable 的执行结果。一个任务可能会由于一个异常而结束,因此它可能没有 “成功”。无法通过一个 Future 对象来告知我们是两种结束中的哪一种。以下是一个代码示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
List<Future<String>> futures = executorService.invokeAll(callables);
for(Future<String> future : futures){
System.out.println("future.get = " + future.get());
}
//ExecutorService 关闭
executorService.shutdown();
2.6 ExecutorService 关闭
使用完 ExecutorService 之后应该将其关闭,以使其中的线程不再运行。
比如,如果应用是通过一个 main() 方法启动的,之后 main 方法退出了应用,如果应用有一个活动的 ExexutorService 它将还会保持运行。ExecutorService 里的活动线程阻止了 JVM 的关闭。
要终止 ExecutorService 里的线程需要调用 ExecutorService 的 shutdown() 方法。ExecutorService 并不会立即关闭,但它将不再接受新的任务,而且一旦所有线程都完成了当前任务的时候,ExecutorService 将会关闭。在 shutdown() 被调用之前所有提交给 ExecutorService 的任务都被执行。
如果想要立即关闭 ExecutorService,可以调用 shutdownNow() 方法。这样会立即尝试停止所有执行中的任务,并忽略掉那些已提交但尚未开始处理的任务。无法担保执行任务的正确执行。可能它们被停止了,也可能已经执行结束。
3、自定义线程池
3.1、为什么要自定义线程池
在开始说自定义线程之前,首先要说明的,既然JDK已经给我们提供了功能强大的Executors工具类来创建四类线程,那为什么还需要自定义线程呢?
这里我们可以参考阿里巴巴java开发手册,在第一章编程规约第4点中,阿里巴巴规约给出了解释得很清楚,这里贴一下该条规约:
- 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
如下为这四类线程的构造方法,在其中的注释中对上述的解释有所标注,对于线程池七大参数的意义,这里不再赘述,如有不明白的地方,可以打开ThreadPoolExecutor的构造器源码看看
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());//该阻塞队列可以无限扩张
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));//该阻塞队列可以无限扩张
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,//线程池的数量可以无限扩张
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,//线程池的数量可以无限扩张
0, NANOSECONDS,
new DelayedWorkQueue());
}
3.2、自定义线程池原则
在我们自定义线程池的过程中,除了可能会使内存OOM异常以外,我们更应该关注我们系统的稳定性,使得系统能高效的支持线程的负载,要使用好线程池,那么一定要遵循几个规则:
- 线程个数大小的设置
首先针对于这个问题,我们必须要明确我们的需求是计算密集型还是IO密集型,只有了解了这一点,我们才能更好的去设置线程池的数量进行限制。
- 计算密集型:
顾名思义就是应用需要非常多的CPU计算资源,在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:线程数 = CPU核数+1,也可以设置成CPU核数*2,但还要看JDK的版本以及CPU配置(服务器的CPU有超线程)。一般设置CPU * 2即可。 - IO密集型:
我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。那么这个线程池的数据量是不是可以随便设置呢?当然不是的,请一定要记得,线程上下文切换是有代价的。目前总结了一套公式,对于IO密集型应用:线程数 = CPU核心数/(1-阻塞系数) 这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9。套用公式,对于双核CPU来说,它比较理想的线程数就是20,当然这都不是绝对的,需要根据实际情况以及实际业务来调整:final int poolSize = (int)(cpuCore/(1-0.9))
针对于阻塞系数,《Programming Concurrency on the JVM Mastering》即《Java 虚拟机并发编程》中有提到一句话: 对于阻塞系数,我们可以先试着猜测,抑或采用一些性能分析工具或java.lang.managementAPI 来确定线程花在系统/IO操作上的时间与CPU密集任务所耗的时间比值。
- 线程池相关参数配置
一定不要选择没有上限限制的配置项。
3.2、自定义线程池
package com.utils;
/**
* Created by xsl on 2020/7/31 10:57
*/
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName ThreadPool
* @Description: 线程池 单例与代理模式
* @Author xsl
* @Date 2020/7/31 10:57
**/
public class ThreadPool {
/**
* 初始化核心线程数
*/
private static final int processors = Runtime.getRuntime().availableProcessors();
/**
* 线程池最大容量
*/
private static int maxPoolSize = processors * 2;
/**
* 扩容线程数闲置寿命
*/
private static int keepAliveTime = 2;
/**
* 等待队列长度
*/
private static int waitQueueSize = 1000;
/**
* 拒绝策略
*/
private static RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
/**
* 线程池实例,懒汉式
*/
private static ThreadPool instance = null;
/**
* 可重用锁
*/
private static ReentrantLock lock = new ReentrantLock();
/**
* 线程池
*/
private static ExecutorService fixedThreadPool = null;
private ThreadPool() {
//私有构造器
}
public ExecutorService getExecutorService() {
return fixedThreadPool;
}
public static ThreadPool getInstance() {
//双重判断加锁
if (instance == null) {
lock.lock();
if (instance == null) {
instance = new ThreadPool();
//使用ThreadPoolExecutor不要使用Executors
fixedThreadPool = new ThreadPoolExecutor(
processors,
maxPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(waitQueueSize),
Executors.defaultThreadFactory(),
handler
);
}
lock.unlock();
}
return instance;
}
}
4、有返回值线程
Java Callable 接口 java.util.concurrent.Callable 表示可以由单独的线程执行的异步任务。例如,可以将 Callable对象提交给 Java ExecutorService,然后 Java ExecutorService 将异步执行它。
4.1、Callable接口定义
Java Callable接口非常简单。它包含一个名为 call() 的方法。以下是 Callable 接口:
public interface Callable<V> {
V call() throws Exception;
}
复制代码调用 call() 方法以执行异步任务。call() 方法可以返回结果。如果任务是异步执行的,则结果通常通过 Java Future传播给任务的创建者。当一个 Callable 被提交给 ExecutorService 并发执行时就是可以使用 Future对象接收返回结果 。
如果任务在执行期间失败, call() 方法也可以抛出 Exception。
4.2、接口实现
这是一个实现 Java Callable 接口的简单示例:
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return String.valueOf(System.currentTimeMillis());
}
}
这个实现非常简单。结果是 call() 方法将返回一个当前时间的 String 。在实际应用程序中,任务可能是更复杂或更大的操作集。
通常,IO操作(如读取或写入磁盘或网络)是可以同时执行的任务。IO操作通常在读取和写入数据块之间有很长的等待时间。通过在单独的线程中执行此类任务,可以避免不必要地阻塞主线程。
4.3、Callable与Runnable
Java Callable 接口类似于 Java Runnable 接口,因为它们都表示可以由单独的线程并发执行的任务。
一个Java Callable 是从不同 Runnable 在于该 Runnable 接口的 run() 方法不返回一个值,而且它不能抛出checked异常(仅 RuntimeException )。
此外,Runnable 最初设计用于长时间运行的并发任务,例如同时运行的网络服务器,或者查看新文件的目录。Callable 接口更适用于返回单个结果的一次性任务。
4、线程执行结果:Java Future
java Future,java.util.concurrent.Future 表示异步计算的结果。创建异步任务时,将 Future 作为线程返回的Java 对象。此 Future 对象用作异步任务结果的句柄。异步任务完成后,可以通过 Future 对象访问结果。
Java的一些内置并发服务(例如Java ExecutorService)从其某些方法返回 Future 对象。在这种情况下ExecutorService 会在提交并执行一个Callable任务时返回一个 Future 对象。
4.1、接口定义
为了了解Java Future 接口的工作原理,接口定义:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning)
V get();
V get(long timeout, TimeUnit unit);
boolean isCancelled();
boolean isDone();
}
4.2、从Future获取结果
如前所述,Java Future 表示异步任务的结果。要获得结果,通过 Future 对象调用两种 get() 方法中的一个。该get() 方法均返回一个 Object,但返回类型也可以是通用的返回类型(指一个特定的类的对象,而不仅仅是一个Object)。以下是 Future 通过其 get() 方法获取结果的示例:
Future future = ... // get Future by starting async task
// do something else, until ready to check result via Future
// get result from Future
try {
Object result = future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
如果在异步任务完成之前调用 get() 方法,则该 get() 方法将阻塞,直到线程执行结束。
第二种 get() 方法可以在经过一段时间后超时返回,可以通过方法参数指定超时时间。以下是调用该 get() 版本的示例:
try {
Object result =
future.get(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
} catch (ExecutionException e) {
} catch (TimeoutException e) {
// thrown if timeout time interval passes
// before a result is available.
}
上面的示例 Future 最多等待1000毫秒。如果在1000毫秒内没有可用结果,则抛出 TimeoutException 异常。
4.3、通过Future取消任务
可以通过调用 Future 的 cancel() 方法取消对应的的异步任务。该异步任务必须是已实现且正在执行的。如果没有,调用 cancel() 将无效。以下是通过 Java Futurecancel() 方法取消任务的示例:
future.cancel();
4.4、检查任务是否完成
可以通过调用 Future isDone() 方法来检查异步任务是否完成(以及结果是否可用)。以下是调用Java Future isDone() 方法的示例:
Future future = ... // Get Future from somewhere
if(future.isDone()) {
Object result = future.get();
} else {
// do something else
}
4.5、检查任务是否被取消
还可以检查Java表示的异步任务是否被取消。可以通过调用 FutureisCancelled() 方法来检查。以下是检查任务是否已取消的示例:
Future future = ... // get Future from somewhere
if(future.isCancelled()) {
} else {
}
4.6、线程池执行Callable实例
package com;
import com.utils.ThreadPool;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class testCallableTest {
public static void main(String[] args) {
//获取线程池实例
ThreadPool instance = ThreadPool.getInstance();
ExecutorService executorService = instance.getExecutorService();
//submit执行
Future<String> submit = executorService.submit(() -> "1234");
try {
//阻塞式接受
String s = submit.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
5、使用 ForkJoinPool 进行分叉和合并
ForkJoinPool 在 Java 7 中被引入。它和 ExecutorService 很相似,除了一点不同。ForkJoinPool 让我们可以很方便地把任务分裂成几个更小的任务,这些分裂出来的任务也将会提交给 ForkJoinPool。任务可以继续分割成更小的子任务,只要它还能分割。可能听起来有些抽象,因此本节中我们将会解释 ForkJoinPool 是如何工作的,还有任务分割是如何进行的。
5.1、分叉和合并解释
在开始看 ForkJoinPool 之前先来简要解释一下分叉和合并的原理。 分叉和合并原理包含两个递归进行的步骤。两个步骤分别是分叉步骤和合并步骤。
5.2 分叉
一个使用了分叉和合并原理的任务可以将自己分叉(分割)为更小的子任务,这些子任务可以被并发执行。如下图所示:
通过把自己分割成多个子任务,每个子任务可以由不同的 CPU 并行执行,或者被同一个 CPU 上的不同线程执行。
只有当给的任务过大,把它分割成几个子任务才有意义。把任务分割成子任务有一定开销,因此对于小型任务,这个分割的消耗可能比每个子任务并发执行的消耗还要大。
什么时候把一个任务分割成子任务是有意义的,这个界限也称作一个阀值。这要看每个任务对有意义阀值的决定。很大程度上取决于它要做的工作的种类。
5.3、合并
当一个任务将自己分割成若干子任务之后,该任务将进入等待所有子任务的结束之中。一旦子任务执行结束,该任务可以把所有结果合并到同一个结果。图示如下:
当然,并非所有类型的任务都会返回一个结果。如果这个任务并不返回一个结果,它只需等待所有子任务执行完毕。也就不需要结果的合并啦。
5.4、ForkJoinPool
ForkJoinPool 是一个特殊的线程池,它的设计是为了更好的配合 分叉-和-合并 任务分割的工作。ForkJoinPool 也在 java.util.concurrent 包中,其完整类名为 java.util.concurrent.ForkJoinPool。
5.5、创建一个 ForkJoinPool
可以通过其构造子创建一个 ForkJoinPool。作为传递给 ForkJoinPool 构造子的一个参数,可以定义期望的并行级别。并行级别表示你想要传递给 ForkJoinPool 的任务所需的线程或 CPU 数量。以下是一个 ForkJoinPool 示例:
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
这个示例创建了一个并行级别为 4 的 ForkJoinPool。
5.6、提交任务到 ForkJoinPool
就像提交任务到 ExecutorService 那样,把任务提交到 ForkJoinPool。你可以提交两种类型的任务。一种是没有任何返回值的(一个 “行动”),另一种是有返回值的(一个”任务”)。这两种类型分别由 RecursiveAction 和 RecursiveTask 表示。接下来介绍如何使用这两种类型的任务,以及如何对它们进行提交。
5.7、RecursiveAction
RecursiveAction 是一种没有任何返回值的任务。它只是做一些工作,比如写数据到磁盘,然后就退出了。
一个 RecursiveAction 可以把自己的工作分割成更小的几块,这样它们可以由独立的线程或者 CPU 执行。
你可以通过继承来实现一个 RecursiveAction。示例如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveAction;
public class MyRecursiveAction extends RecursiveAction {
private long workLoad = 0;
public MyRecursiveAction(long workLoad) {
this.workLoad = workLoad;
}
@Override
protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
subtasks.addAll(createSubtasks());
for(RecursiveAction subtask : subtasks){
subtask.fork();
}
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
}
}
private List<MyRecursiveAction> createSubtasks() {
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
}
例子很简单。MyRecursiveAction 将一个虚构的 workLoad 作为参数传给自己的构造子。如果 workLoad 高于一个特定阀值,该工作将被分割为几个子工作,子工作继续分割。如果 workLoad 低于特定阀值,该工作将由 MyRecursiveAction 自己执行。
可以这样规划一个 MyRecursiveAction 的执行:
MyRecursiveAction myRecursiveAction = new MyRecursiveAction(24);
forkJoinPool.invoke(myRecursiveAction);
5.8、RecursiveTask
RecursiveTask 是一种会返回结果的任务。它可以将自己的工作分割为若干更小任务,并将这些子任务的执行结果合并到一个集体结果。可以有几个水平的分割和合并。以下是一个 RecursiveTask 示例:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveTask;
public class MyRecursiveTask extends RecursiveTask<Long> {
private long workLoad = 0;
public MyRecursiveTask(long workLoad) {
this.workLoad = workLoad;
}
protected Long compute() {
//if work is above threshold, break tasks up into smaller tasks
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
subtasks.addAll(createSubtasks());
for(MyRecursiveTask subtask : subtasks){
subtask.fork();
}
long result = 0;
for(MyRecursiveTask subtask : subtasks) {
result += subtask.join();
}
return result;
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
return workLoad * 3;
}
}
private List<MyRecursiveTask> createSubtasks() {
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
}
除了有一个结果返回之外,这个示例和 RecursiveAction 的例子很像。MyRecursiveTask 类继承自 RecursiveTask,这也就意味着它将返回一个 Long 类型的结果。
MyRecursiveTask 示例也会将工作分割为子任务,并通过 fork() 方法对这些子任务计划执行。
此外,本示例还通过调用每个子任务的 join() 方法收集它们返回的结果。子任务的结果随后被合并到一个更大的结果,并最终将其返回。对于不同级别的递归,这种子任务的结果合并可能会发生递归。
你可以这样规划一个 RecursiveTask:
MyRecursiveTask myRecursiveTask = new MyRecursiveTask(128);
long mergedResult = forkJoinPool.invoke(myRecursiveTask);
System.out.println("mergedResult = " + mergedResult); 12345
注意是如何通过 ForkJoinPool.invoke() 方法的调用来获取最终执行结果的。
5.9、ForkJoinPool 评论
貌似并非每个人都对 Java 7 里的 ForkJoinPool 满意:《一个 Java 分叉-合并 带来的灾祸》。
在你计划在自己的项目里使用 ForkJoinPool 之前最好读一下该篇文章。
四、并发容器
1、CopyOnWrite
CopyOnWrite–写时复制容器是一种常用的并发容器,它通过多线程下读写分离来达到提高并发性能的目的,和前面我们讲解StampedLock时所用的解决方案类似:任何时候都可以进行读操作,写操作则需要加锁。不同的是,在CopyOnWrite中,对容器的修改操作加锁后,通过copy一个新的容器来进行修改,修改完毕后将容器替换为新的容器即可。
这种方式的好处显而易见:通过copy一个新的容器来进行修改,这样读操作就不需要加锁,可以并发读,因为在读的过程中是采用的旧的容器,即使新容器做了修改对旧容器也没有影响,同时也很好的解决了迭代过程中其他线程修改导致的并发问题。
JDK中提供的并发容器包括CopyOnWriteArrayList和CopyOnWriteArraySet,下面通过CopyOnWriteArrayList的部分源码来理解这种思想:
//添加元素
public boolean add(E e) {
//独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//复制一个新的数组newElements
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//修改后指向新的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public E get(int index) {
//未加锁,直接获取
return get(getArray(), index);
}
代码很简单,在add操作中通过一个共享的ReentrantLock来获取锁,这样可以防止多线程下多个线程同时修改容器内容。获取锁后通过Arrays.copyOf复制了一个新的容器,然后对新的容器进行了修改,最后直接通过setArray将原数组引用指向了新的数组,避免了在修改过程中迭代数据出现错误。get操作由于是读操作,未加锁,直接读取就行。CopyOnWriteArraySet类似,这里不做过多讨论。
CopyOnWrite容器虽然在多线程下使用是安全的,相比较Vector也大大提高了读写的性能,但它也有自身的问题。
首先就是性能,在讲解ArrayList的文章中提到过,ArrayList的扩容由于使用了Arrays.copyOf每次都需要申请更大的空间以及复制现有的元素到新的数组,对性能存在一定影响。CopyOnWrite容器也不例外,每次修改操作都会申请新的数组空间,然后进行替换。所以在高并发频繁修改容器的情况下,会不断申请新的空间,同时会造成频繁的GC,这时使用CopyOnWrite容器并不是一个好的选择。
其次还有一个数据一致性问题,由于在修改中copy了新的数组进行替换,同时旧数组如果还在被使用,那么新的数据就不能被及时读取到,这样就造成了数据不一致,如果需要强数据一致性,CopyOnWrite容器也不太适合。
2、ConcurrentMap
2.1 java.util.concurrent.ConcurrentMap
java.util.concurrent.ConcurrentMap 接口表示了一个能够对别人的访问(插入和提取)进行并发处理的 java.util.Map。
ConcurrentMap 除了从其父接口 java.util.Map 继承来的方法之外还有一些额外的原子性方法。
2.2 ConcurrentMap的实现
既然 ConcurrentMap 是个接口,你想要使用它的话就得使用它的实现类之一。java.util.concurrent 包具备 ConcurrentMap 接口的以下实现类:ConcurrentHashMap
2.3 ConcurrentHashMap
ConcurrentHashMap 和 java.util.HashTable 类很相似,但 ConcurrentHashMap 能够提供比 HashTable 更好的并发性能。在你从中读取对象的时候 ConcurrentHashMap 并不会把整个 Map 锁住。此外,在你向其中写入对象的时候,ConcurrentHashMap 也不会锁住整个 Map。它的内部只是把 Map 中正在被写入的部分进行锁定。
另外一个不同点是,在被遍历的时候,即使是 ConcurrentHashMap 被改动,它也不会抛ConcurrentModificationException。尽管 Iterator 的设计不是为多个线程的同时使用。
更多关于 ConcurrentMap 和 ConcurrentHashMap 的细节请参考官方文档。
2.4 ConcurrentMap 例子
以下是如何使用 ConcurrentMap 接口的一个例子。本示例使用了 ConcurrentHashMap 实现类:
ConcurrentMap concurrentMap = new ConcurrentHashMap();
concurrentMap.put("key", "value");
Object value = concurrentMap.get("key");
3、并发导航映射 ConcurrentNavigableMap
java.util.concurrent.ConcurrentNavigableMap 是一个支持并发访问的 java.util.NavigableMap,它还能让它的子 map 具备并发访问的能力。所谓的 “子 map” 指的是诸如 headMap(),subMap(),tailMap() 之类的方法返回的 map。
NavigableMap 中的方法不再赘述,本小节我们来看一下 ConcurrentNavigableMap 添加的方法。
3.1、headMap()
headMap(T toKey) 方法返回一个包含了小于给定 toKey 的 key 的子 map。
如果你对原始 map 里的元素做了改动,这些改动将影响到子 map 中的元素(译者注:map 集合持有的其实只是对象的引用)。
以下示例演示了对 headMap() 方法的使用:
ConcurrentNavigableMap map = new ConcurrentSkipListMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
ConcurrentNavigableMap headMap = map.headMap("2");
headMap 将指向一个只含有键 “1” 的 ConcurrentNavigableMap,因为只有这一个键小于 “2”。关于这个方法及其重载版本具体是怎么工作的细节请参考 Java 文档。
3.2、tailMap()
tailMap(T fromKey) 方法返回一个包含了不小于给定 fromKey 的 key 的子 map。
如果你对原始 map 里的元素做了改动,这些改动将影响到子 map 中的元素(译者注:map 集合持有的其实只是对象的引用)。
以下示例演示了对 tailMap() 方法的使用:
ConcurrentNavigableMap map = new ConcurrentSkipListMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
ConcurrentNavigableMap tailMap = map.tailMap("2");
tailMap 将拥有键 “2” 和 “3”,因为它们不小于给定键 “2”。关于这个方法及其重载版本具体是怎么工作的细节请参考 Java 文档。
3.3、subMap()
subMap() 方法返回原始 map 中,键介于 from(包含) 和 to (不包含) 之间的子 map。示例如下:
ConcurrentNavigableMap map = new ConcurrentSkipListMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
ConcurrentNavigableMap subMap = map.subMap("2", "3");
返回的 submap 只包含键 “2”,因为只有它满足不小于 “2”,比 “3” 小。
10.4 更多方法
ConcurrentNavigableMap 接口还有其他一些方法可供使用,比如:
- descendingKeySet()
- descendingMap()
- navigableKeySet()
关于这些方法更多信息参考官方 Java 文档。
ConcurrentSkipListMap
java.util中对应的容器在java.util.concurrent包中基本都可以找到对应的并发容器:List和Set有对应的CopyOnWriteArrayList与CopyOnWriteArraySet,HashMap有对应的ConcurrentHashMap,但是有序的TreeMap或并没有对应的ConcurrentTreeMap。为什么没有ConcurrentTreeMap呢?这是因为TreeMap内部使用了红黑树来实现,红黑树是一种自平衡的二叉树,当树被修改时,需要重新平衡,重新平衡操作可能会影响树的大部分节点,如果并发量非常大的情况下,这就需要在许多树节点上添加互斥锁,那并发就失去了意义。所以提供了另外一种并发下的有序map实现:ConcurrentSkipListMap。
ConcurrentSkipListMap内部使用跳表(SkipList)这种数据结构来实现,他的结构相对红黑树来说非常简单理解,实现起来也相对简单,而且在理论上它的查找、插入、删除时间复杂度都为log(n)。在并发上,ConcurrentSkipListMap采用无锁的CAS+自旋来控制。
跳表简单来说就是一个多层的链表,底层是一个普通的链表,然后逐层减少,通常通过一个简单的算法实现每一层元素是下一层的元素的二分之一,这样当搜索元素时从最顶层开始搜索,可以说是另一种形式的二分查找。
一个简单的获取跳表层数概率算法实现如下:
int random_level() {
K = 1;
while (random(0,1))
K++;
return K;
}
通过简单的0和1获取概率,1层的概率为50%,2层的概率为25%,3层的概率为12.5%,这样逐级递减。
一个三层的跳表添加元素的过程如下:
插入值为15的节点:
插入后:
维基百科中有一个添加节点的动图,这里也贴出来方便理解:
通过分析ConcurrentSkipListMap的put方法来理解跳表以及CAS自旋并发控制:
private V doPut(K key, V value, boolean onlyIfAbsent) {
Node<K,V> z; // added node
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
outer: for (;;) {
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
//查找前继节点
if (n != null) {
//查找到前继节点
Object v; int c;
Node<K,V> f = n.next; //获取后继节点的后继节点
if (n != b.next) //发生竞争,两次节点获取不一致,并发导致
break;
if ((v = n.value) == null) {
// 节点已经被删除
n.helpDelete(b, f);
break;
}
if (b.value == null || v == n)
break;
if ((c = cpr(cmp, key, n.key)) > 0) {
//进行下一轮查找,比当前key大
b = n;
n = f;
continue;
}
if (c == 0) {
//相等时直接cas修改值
if (onlyIfAbsent || n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
z = new Node<K,V>(key, value, n); //9. n.key > key > b.key
if (!b.casNext(n, z)) //cas修改值
break; // restart if lost race to append to b
break outer;
}
}
int rnd = ThreadLocalRandom.nextSecondarySeed(); //获取随机数
if ((rnd & 0x80000001) == 0) {
// test highest and lowest bits
int level = 1, max;
while (((rnd >>>= 1) & 1) != 0) // 获取跳表层级
++level;
Index<K,V> idx = null;
HeadIndex<K,V> h = head;
if (level <= (max = h.level)) {
//如果获取的调表层级小于等于当前最大层级,则直接添加,并将它们组成一个上下的链表
for (int i = 1; i <= level; ++i)
idx = new Index<K,V>(z, idx, null);
}
else {
// try to grow by one level //否则增加一层level,在这里体现为Index<K,V>数组
level = max + 1; // hold in array and later pick the one to use
@SuppressWarnings("unchecked")Index<K,V>[] idxs =
(Index<K,V>[])new Index<?,?>[level+1];
for (int i = 1; i <= level; ++i)
idxs[i] = idx = new Index<K,V>(z, idx, null);
for (;;) {
h = head;
int oldLevel = h.level;
if (level <= oldLevel) // lost race to add level
break;
HeadIndex<K,V> newh = h;
Node<K,V> oldbase = h.node;
for (int j = oldLevel+1; j <= level; ++j) //新添加的level层的具体数据
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
if (casHead(h, newh)) {
h = newh;
idx = idxs[level = oldLevel];
break;
}
}
}
// 逐层插入数据过程
splice: for (int insertionLevel = level;;) {
int j = h.level;
for (Index<K,V> q = h, r = q.right, t = idx;;) {
if (q == null || t == null)
break splice;
if (r != null) {
Node<K,V> n = r.node;
// compare before deletion check avoids needing recheck
int c = cpr(cmp, key, n.key);
if (n.value == null) {
if (!q.unlink(r))
break;
r = q.right;
continue;
}
if (c > 0) {
q = r;
r = r.right;
continue;
}
}
if (j == insertionLevel) {
if (!q.link(r, t))
break; // restart
if (t.node.value == null) {
findNode(key);
break splice;
}
if (--insertionLevel == 0)
break splice;
}
if (--j >= insertionLevel && j < level)
t = t.down;
q = q.down;
r = q.right;
}
}
}
return null;
}
这里的插入方法很复杂,可以分为3大步来理解:第一步获取前继节点后通过CAS来插入节点;第二步对level层数进行判断,如果大于最大层数,则插入一层;第三步插入对应层的数据。整个插入过程全部通过CAS自旋的方式保证并发情况下的数据正确性。
五、线程执行控制辅助类
1、闭锁 CountDownLatch
java.util.concurrent.CountDownLatch 是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。
CountDownLatch 以一个给定的数量初始化。countDown() 每被调用一次,这一数量就减一。通过调用 await() 方法之一,线程可以阻塞等待这一数量到达零。
以下是一个简单示例。Decrementer 三次调用 countDown() 之后,等待中的 Waiter 才会从 await() 调用中释放出来。
CountDownLatch latch = new CountDownLatch(3);
Waiter waiter = new Waiter(latch);
Decrementer decrementer = new Decrementer(latch);
new Thread(waiter).start();
new Thread(decrementer).start();
Thread.sleep(4000);
public class Waiter implements Runnable{
CountDownLatch latch = null;
public Waiter(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Waiter Released");
}
}
public class Decrementer implements Runnable {
CountDownLatch latch = null;
public Decrementer(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
Thread.sleep(1000);
this.latch.countDown();
Thread.sleep(1000);
this.latch.countDown();
Thread.sleep(1000);
this.latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2、栅栏CyclicBarrier
java.util.concurrent.CyclicBarrier 类是一种同步机制,它能够对处理一些算法的线程实现同步。换句话讲,它就是一个所有线程必须等待的一个栅栏,直到所有线程都到达这里,然后所有线程才可以继续做其他事情。图示如下:
两个线程在栅栏旁等待对方。 通过调用 CyclicBarrier 对象的 await() 方法,两个线程可以实现互相等待。一旦 N 个线程在等待 CyclicBarrier 达成,所有线程将被释放掉去继续运行。
2.1、创建一个 CyclicBarrier
在创建一个 CyclicBarrier 的时候你需要定义有多少线程在被释放之前等待栅栏。创建 CyclicBarrier 示例:
CyclicBarrier barrier = new CyclicBarrier(2);
2.2、等待一个 CyclicBarrier
以下演示了如何让一个线程等待一个 CyclicBarrier:
barrier.await();
当然,你也可以为等待线程设定一个超时时间。等待超过了超时时间之后,即便还没有达成 N 个线程等待 CyclicBarrier 的条件,该线程也会被释放出来。以下是定义超时时间示例:
barrier.await(10, TimeUnit.SECONDS);
满足以下任何条件都可以让等待 CyclicBarrier 的线程释放:
- 最后一个线程也到达 CyclicBarrier(调用 await())
- 当前线程被其他线程打断(其他线程调用了这个线程的 interrupt() 方法)
- 其他等待栅栏的线程被打断
- 其他等待栅栏的线程因超时而被释放
- 外部线程调用了栅栏的 CyclicBarrier.reset() 方法
2.3、CyclicBarrier 行动
CyclicBarrier 支持一个栅栏行动,栅栏行动是一个 Runnable 实例,一旦最后等待栅栏的线程抵达,该实例将被执行。你可以在 CyclicBarrier 的构造方法中将 Runnable 栅栏行动传给它:
Runnable barrierAction = ... ;
CyclicBarrier barrier = new CyclicBarrier(2, barrierAction);
2.4 CyclicBarrier 示例
以下代码演示了如何使用 CyclicBarrier:
Runnable barrier1Action = new Runnable() {
public void run() {
System.out.println("BarrierAction 1 executed ");
}
};
Runnable barrier2Action = new Runnable() {
public void run() {
System.out.println("BarrierAction 2 executed ");
}
};
CyclicBarrier barrier1 = new CyclicBarrier(2, barrier1Action);
CyclicBarrier barrier2 = new CyclicBarrier(2, barrier2Action);
CyclicBarrierRunnable barrierRunnable1 = new CyclicBarrierRunnable(barrier1, barrier2);
CyclicBarrierRunnable barrierRunnable2 = new CyclicBarrierRunnable(barrier1, barrier2);
new Thread(barrierRunnable1).start();
new Thread(barrierRunnable2).start();
CyclicBarrierRunnable 类:
public class CyclicBarrierRunnable implements Runnable{
CyclicBarrier barrier1 = null;
CyclicBarrier barrier2 = null;
public CyclicBarrierRunnable(
CyclicBarrier barrier1,
CyclicBarrier barrier2) {
this.barrier1 = barrier1;
this.barrier2 = barrier2;
}
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +
" waiting at barrier 1");
this.barrier1.await();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +
" waiting at barrier 2");
this.barrier2.await();
System.out.println(Thread.currentThread().getName() +
" done!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
以上代码控制台输出如下。注意每个线程写入控制台的时序可能会跟你实际执行不一样。比如有时 Thread-0 先打印,有时 Thread-1 先打印。
Thread-0 waiting at barrier 1
Thread-1 waiting at barrier 1
BarrierAction 1 executed
Thread-1 waiting at barrier 2
Thread-0 waiting at barrier 2
BarrierAction 2 executed
Thread-0 done!
Thread-1 done!12345678
3、交换机 Exchanger
java.util.concurrent.Exchanger 类表示一种两个线程可以进行互相交换对象的会和点。这种机制图示如下:
两个线程通过一个 Exchanger 交换对象。
交换对象的动作由 Exchanger 的两个 exchange() 方法的其中一个完成。以下是一个示例:
Exchanger exchanger = new Exchanger();
ExchangerRunnable exchangerRunnable1 = new ExchangerRunnable(exchanger, "A");
ExchangerRunnable exchangerRunnable2 = new ExchangerRunnable(exchanger, "B");
new Thread(exchangerRunnable1).start();
new Thread(exchangerRunnable2).start();
ExchangerRunnable 代码:
public class ExchangerRunnable implements Runnable{
Exchanger exchanger = null;
Object object = null;
public ExchangerRunnable(Exchanger exchanger, Object object) {
this.exchanger = exchanger;
this.object = object;
}
public void run() {
try {
Object previous = this.object;
this.object = this.exchanger.exchange(this.object);
System.out.println(
Thread.currentThread().getName() +
" exchanged " + previous + " for " + this.object
);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以上程序输出:
Thread-0 exchanged A for B
Thread-1 exchanged B for A12
4、信号量 Semaphore
java.util.concurrent.Semaphore 类是一个计数信号量。这就意味着它具备两个主要方法:
acquire()
release()
计数信号量由一个指定数量的 “许可” 初始化。每调用一次 acquire(),一个许可会被调用线程取走。每调用一次 release(),一个许可会被返还给信号量。因此,在没有任何 release() 调用时,最多有 N 个线程能够通过 acquire() 方法,N 是该信号量初始化时的许可的指定数量。这些许可只是一个简单的计数器。这里没啥奇特的地方。
4.1、Semaphore 用法
信号量主要有两种用途:
- 保护一个重要(代码)部分防止一次超过 N 个线程进入
- 在两个线程之间发送信号
4.2、保护重要部分
如果你将信号量用于保护一个重要部分,试图进入这一部分的代码通常会首先尝试获得一个许可,然后才能进入重要部分(代码块),执行完之后,再把许可释放掉。比如这样:
Semaphore semaphore = new Semaphore(1);
//critical section
semaphore.acquire();
...
semaphore.release();
4.3、在线程之间发送信号
如果你将一个信号量用于在两个线程之间传送信号,通常你应该用一个线程调用 acquire() 方法,而另一个线程调用 release() 方法。
如果没有可用的许可,acquire() 调用将会阻塞,直到一个许可被另一个线程释放出来。同理,如果无法往信号量释放更多许可时,一个 release() 调用也会阻塞。
通过这个可以对多个线程进行协调。比如,如果线程 1 将一个对象插入到了一个共享列表(list)之后之后调用了 acquire(),而线程 2 则在从该列表中获取一个对象之前调用了 release(),这时你其实已经创建了一个阻塞队列。信号量中可用的许可的数量也就等同于该阻塞队列能够持有的元素个数。
4.4、公平
没有办法保证线程能够公平地可从信号量中获得许可。也就是说,无法担保掉第一个调用 acquire() 的线程会是第一个获得一个许可的线程。如果第一个线程在等待一个许可时发生阻塞,而第二个线程前来索要一个许可的时候刚好有一个许可被释放出来,那么它就可能会在第一个线程之前获得许可。
如果你想要强制公平,Semaphore 类有一个具有一个布尔类型的参数的构造子,通过这个参数以告知 Semaphore 是否要强制公平。强制公平会影响到并发性能,所以除非你确实需要它否则不要启用它。
以下是如何在公平模式创建一个 Semaphore 的示例:
Semaphore semaphore = new Semaphore(1, true);
4.5、更多方法
java.util.concurrent.Semaphore 类还有很多方法,比如:
- availablePermits()
- acquireUninterruptibly()
- drainPermits()
- hasQueuedThreads()
- getQueuedThreads()
- tryAcquire()