JUC并发编程
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。
进程和线程
进程:具有一定独立功能的程序关于某个数据集合的一次运行活动。进程是资源调度的基本单位;
线程:通常在一个进程中可以包含若干个线程,一个进程中至少有一个主线程。线程是分配资源的基本单位。线程比进程更小,基本上不拥有系统资源,线程的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
并发和并行
并发:一段时间内多个任务在宏观上同时执行,微观上采用时间片轮转切换执行;
并行:同一时间点多个任务同时执行,并行是针对多核CPU提出的概念;
1 线程创建和线程状态
1.1 实现Runnable接口
通过实现Runnable接口中的run方法,可以定义一个任务,之后通过传给Thread类的构造函数就可以创建一个可运行的线程对象;也可以不传线程名,系统会默认分配线程名;
class Task implements Runnable {
@Override
public void run() {
}
}
Thread thread = new Thread(new Task(), "thread name");
通常可以使用匿名内部类的方式来创建
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
}
}, "thread name");
进一步简化可以使用lambda表达式来创建
Thread thread = new Thread(() -> {
}, "thread name");
1.2 线程的状态
线程有6种状态:
- New(新创建):当使用
new Thread创建线程对象时,此时线程是新创建状态; - Runnable(可运行):当调用
thread.start()方法时,线程就处于可运行状态; - Blocked(被阻塞):当线程获取锁、等待条件或调用超时参数方法(例如
Thread.sleep)时,线程处于阻塞状态; - Waiting(等待)
- Timed waiting(计时等待)
- Terminated(被终止)
2、线程同步
当多个线程对共享数据存取时,会因为线程访问数据的顺序不同,而导致程序不符合逻辑预期的结果,此时需要程序员自己使用一些同步策略来保证程序正常运行;
考虑下面的售票的例子,有一个共享的数据count,可以调用sale方法进行售票;sale方法的步骤为:
- (1)判断余票是否充足;
- (2)如果充足,将余票数减1;
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 40; j++) {
ticket.sale();
}
}, "Saler " + i).start();
}
}
}
class Ticket{
private int count = 30;
private Random random = new Random();
public void sale(){
try {
if (count > 0) {
Thread.sleep((int) (Math.random() * 50));
count--;
System.out.println(Thread.currentThread().getName() + " 卖出, 剩余:" + count);
} else {
System.out.println(Thread.currentThread().getName() + " 未卖出, 剩余:" + count);
}
} catch (Exception e){
}
}
}
考虑下面这种情形:当票数只剩1时,两个售票者都判断count > 0,此时就会出现超卖问题;

2.1 锁对象
针对上述问题,可以利用juc的锁对象来解决;只要保证sale操作的原子性就不会出现超卖问题,在判断之前加锁,count--操作之后释放锁,就可以达到原子性。
juc的Lock接口定义了如下方法:
lock方法是阻塞式的,当获取锁失败时会导致线程阻塞,直至其他线程释放锁;tryLock会尝试获取锁,如果获取锁失败不会阻塞,只会返回false。
public interface Lock {
void lock();
void unlock();
boolean tryLock();
}
juc主要有可重入锁和读写锁,两者都实现了Lock接口;
2.1.1 可重入锁ReentrantLock
可重入锁的意思是同一个线程可以重复获得锁,当然,获得几次锁就需要释放几次;
可重入锁提供给了两种构造函数,第二种构建一个带有公平策略的锁,当传入true时,会构造一个更偏向于等待时间长的线程,这种方式会造成性能的下降;
ReentrantLock();
ReentrantLock(boolean fair);
使用可重入锁解决超卖问题的措施如下,改写Ticket类:
class Ticket{
private int count = 30;
private Random random = new Random();
private Lock lock = new ReentrantLock();
public void sale(){
try {
// 加锁
lock.lock();
if (count > 0) {
Thread.sleep((int) (Math.random() * 50));
count--;
System.out.println(Thread.currentThread().getName() + " 卖出, 剩余:" + count);
} else {
System.out.println(Thread.currentThread().getName() + " 未卖出, 剩余:" + count);
}
} catch (Exception e){
} finally {
// 释放锁
lock.unlock();
}
}
}
2.1.2 读写锁
可重入锁一旦某个线程获得锁,其他线程将无法再获得锁。但是如果有很多线程需要读取数据而少量线程修改数据时,这种情况下再使用可重入锁会造成性能浪费。此时使用读写锁,读读共享、读写/写写互斥;
读写锁ReentrantReadWriteLock可以获得读锁和写锁,二者都实现了Lock接口,可以获得读锁对象和写锁对象;
public class ReentrantReadWriteLock {
Lock readLock();
Lock writeLock();
}
下面的例子有2个写线程和3个读线程,写线程只能串行,但是读线程都可以获得读锁;
public class ReadWriteLockDemo {
private static ReadWriteLock rwLock = new ReentrantReadWriteLock();
private static Lock rLock = rwLock.readLock();
private static Lock wLock = rwLock.writeLock();
public static void main(String[] args) {
testRWLock();
}
public static void testRWLock(){
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
wLock.lock();
System.out.println(Thread.currentThread().getName() + "开始写");
int sleepMills = (int)(Math.random() * 10) + 5;
Thread.sleep(sleepMills * 1000);
System.out.println(Thread.currentThread().getName() + "写完成");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
wLock.unlock();
}
}, "Writer-" + i).start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
rLock.lock();
System.out.println(Thread.currentThread().getName() + "开始读");
int sleepMills = (int)(Math.random() * 10) + 5;
Thread.sleep(sleepMills * 1000);
System.out.println(Thread.currentThread().getName() + "读完成");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
rLock.unlock();
}
}, "Reader-" + i).start();
}
}
}
锁降级
同一线程如果先获得了读锁,则可以再获得读锁,这个就是锁降级;但是读锁不可升级为写锁;
public static void testLockDesc(){
new Thread(() -> {
wLock.lock();
System.out.println("获得写锁");
rLock.lock();
System.out.println("获得读锁");
rLock.unlock();
wLock.unlock();
}).start();
}
2.2 条件对象
条件对象的使用场景为:当线程进程临界区(获得锁之后),却发现要满足某一条件才能执行,此时可以使用条件对象来管理那些已经获得锁但是需要等待条件满足时才可以继续工作的场景;比如上面的例子中,当售票员发现票量不足时,应该等待票源补充后继续执行售票操作;Lock接口有一个方法Condition newCondition()可以获得条件对象;
条件对象有如下方法:
await方法会释放锁,当有线程调用signal方法时,该线程不能马上接触阻塞,它必须先获得锁。
// 将线程放入到条件的等待集合中
void await();
// 解除该条件等待集中所有线程的阻塞状态
void signalAll();
// 从该条件的等待集中随机选择一个线程解除阻塞
void signal();
对于售票的例子,加入条件对象;现在有10个售票员进行售票,有一个线程会间歇性的补充票源;售票员售票的方法中,当票源不足时会等待补充票源,当补充票源之后会signalAll,此时被阻塞的线程继续判断是否有足够的票,直至可以售票为止;
public class SaleTask {
public static void main(String[] args) {
Ticket ticket = new Ticket();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 40; i1++) {
ticket.sale();
}
}, "Saler-" + i).start();
}
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(20000);
ticket.addCount(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
class Ticket{
private int count = 30;
private Lock lock = new ReentrantLock();
// 获得锁的条件对象
private Condition condition = lock.newCondition();
private Random random = new Random();
public void sale(){
try {
lock.lock();
while (count <= 0) {
System.out.println(Thread.currentThread().getName() + ": 当前票数不足,等待补充票源....");
// 票源不足,等待
condition.await();
}
Thread.sleep((int) (Math.random() * 50));
count--;
System.out.println(Thread.currentThread().getName() + " 卖出, 剩余:" + count);
}catch (Exception e){
} finally {
lock.unlock();
}
}
public void addCount(int n){
try {
lock.lock();
count += n;
System.out.println(Thread.currentThread().getName() + "补充票源...");
// 通知等待的线程
condition.signalAll();
}catch (Exception e){
} finally {
lock.unlock();
}
}
}
3 阻塞队列
对于多线程问题,出了可以通过锁和条件对象来同步之外,还可以使用阻塞队列来实现同步;生产者线程向队列插入元素,当队列满时,生产者线程阻塞;消费者队列则取出元素执行后续操作,当队列空时,消费者线程阻塞。
JUC的BlockingQueue接口定义了阻塞队列;总共有三组方法:
put和take方法是阻塞式的;offer向队列添加元素,当队列满时会立即返回false,poll从队头取出元素,当队列空时返回null,peek返回队头元素但不取出,队列空时返回null;add,remove和element当队列满/空时,会抛出异常;
public interface BlockingQueue<E> extends Queue<E> {
void put(E elem);
E take();
boolean offer(E elem);
E poll();
E peek();
boolean add(E elem);
E remove();
E element();
}
JUC提供了以下几种阻塞队列,都实现了BlockingQueue接口;
3.1 ArrayBlockingQueue
基于数组实现;它提供了两种构造函数,capacity表示队列的上限,fair表示是否使用公平策略;
ArrayBlockingQueue(int capacity);
ArrayBlockingQueue(int capacity, boolean fair);
3.2 LinkedBlockingQueue
基于链表实现的阻塞队列;提供了两种构造函数,无参构造函数表示构造无上限的阻塞队列;也可以构造一个有上限的阻塞队列,提供capacity参数即可;
LinkedBlockingQueue();
LinkedBlockingQueue(int capacity);
3.3 PriorityBlockingQueue
这是一个带有优先级的队列,而不是先进先出队列。元素按照优先级的顺序被移出。构造函数需要传入一个Comparator,如果不传入,则元素必须实现Comparabe接口;如果使用无参构造函数,则初始容量为11。
PriorityBlockingQueue();
PriorityBlockingQueue(int initialCapacity);
PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator);
3.4 DelayQueue
DelayQueue是一个无界的BlockingQueue;用于放置实现了Delayed接口的对象,其中的对象只能在其过了延迟时间才能从队列中移出。这种队列是有序的,即队头对象的延迟到期时间最长。注意:不能将null元素放置到这种队列中。
3.4.1 延迟队列使用场景
延迟队列可以用在一些需要延迟一定时间后再处理的场景;例如:
- 订单超时取消: 下单之后如果三十分钟之内没有付款就自动取消订单;
- 关闭空闲连接:服务器中,有很多客户端的连接,空闲一段时间之后需要关闭;
- 缓存释放:缓存中的对象,超过了空闲时间,需要从缓存中移出;
- 任务超时处理:在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求等;
3.4.2 延迟队列的使用
延迟队列无法放入普通元素,其放入的元素必须实现Delayed接口。
延迟队列通过getDelay来获取当前元素还有多久过期(可以从对列中取出),返回值大于0表示未过期;同时此接口继承自Comparable接口,实现compare方法时,排序规则必须和getDelay保持一致,因为延迟队列内部是通过优先级队列来实现的,因此它使用的也是小根堆,队列根据compare方法来比较元素,把最小的元素放到堆顶,put和take操作之后都会将最小的元素调整到堆顶;
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
延迟队列只有无参构造函数,会创建一个无上限的延迟队列;
DelayQueue();
下面的例子模拟订单超时自动取消的例子,有十个线程放入订单信息到延迟队列,过30s后取消;DelayTask定义了延迟订单取消的任务,实现了getDelay方法,返回的是剩余过期时间;compare方法根据getDelay来进行比较;
public class DelayQueueDemo {
private static DelayQueue<DelayTask> queue = new DelayQueue<>();
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
DelayTask data = queue.take();
System.out.println(Thread.currentThread().getName() + "取消订单:" + data.getOrderId());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "Consumer").start();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
queue.put(new DelayTask(UUID.randomUUID().toString(), 30 * 1000));
}, "Producer-" + i).start();
try {
Thread.sleep(new Random().nextInt(5000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class DelayTask implements Delayed {
private long start;
private String orderId;
private long delayMills;
public DelayTask(String orderId, long dellayMills){
this.orderId = orderId;
this.delayMills = dellayMills;
start = System.currentTimeMillis();
}
public String getOrderId(){
return orderId;
}
@Override
public long getDelay(TimeUnit unit) {
return (start + delayMills) - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
}
3.5 阻塞队列处理异步任务
售票的案例中,真实的业务中票和订单信息存在数据库,在售票时会先判断是否有余票可以售卖,如果有余票可卖,则去更新数据库;但是更新数据库往往比较耗时,并发量大的时候系统性能很差,这时候我们可以使用阻塞队列来异步处理这个过程:
- (1)将余票信息存入缓存,例如redis;这里我们使用一个变量来模拟;
- (2)售票业务去查缓存票量是否充足,如果充足则将购票信息存入阻塞队列然后直接返回购票成功;
- (3)单独开启一个消费者线程从阻塞队列取购票信息,然后去更新数据库;
先定义一个类模拟数据库的一些操作,这里只有count信息,其他信息的操作使用Thread.sleep来模拟操作耗时,假设更新数据库耗时30ms,而查询耗时15ms。
class TicketDatabase {
private int count;
public TicketDatabase(int count){
this.count = count;
}
public void sale(String saler){
count--;
try {
// 模拟更新数据库耗时30ms
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(saler + "更新购票数据库成功, 剩余" + count + "张票...");
}
public int getCount(){
try {
// 模拟查询数据库耗时15ms
Thread.sleep(15);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
return count;
}
}
}
3.5.1 未进行异步处理
假设有10个线程分别进行40次售票处理操作, 总共30张票;每个线程先查数据票量是否充足,再进行数据库更新操作;
整个系统的耗时计算:
- 成功售票耗时: 30 * (300 + 15) = 9450 ms
- 票量不足: (10 * 40 - 30) * 15 = 5550 ms
总耗时:9450 + 5550 = 15000 ms
public class SaleTicket {
private TicketDatabase database = new TicketDatabase(30);
private Lock lock = new ReentrantLock(true);
public static void main(String[] args) {
new SaleTicket().sale();
}
private void sale(){
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 40; j++) {
try {
lock.lock();
if(database.getCount() > 0){
database.sale(Thread.currentThread().getName());
}
} catch (Exception e){
} finally {
lock.unlock();
}
}
}, "Saler-" + i).start();
}
}
}
3.5.2 使用异步处理
先定义一个缓存类来模拟redis,缓存的操作时间假设为5ms;
class TicketCache {
private int count;
public TicketCache(int count){
this.count = count;
}
public int getCount(){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return count;
}
public void countDesc(){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count--;
}
}
单独去处理数据库的线程的操作时间不算在业务的时间中,因为售票的线程已经进行了缓存更新并告诉用户购票结果了,Handler线程可以慢慢运行;计算整个系统的耗时:
- 成功售票耗时: 30 * (5 + 5) = 300 ms
- 票量不足: (10 * 40 - 30) * 5 = 1850 ms
总耗时:300 + 1850 = 2150 ms, 可以看到整个业务的性能大大提升;
public class SaleTicket {
private TicketDatabase database = new TicketDatabase(30);
private TicketCache cache = new TicketCache(30);
private Lock lock = new ReentrantLock(true);
private BlockingQueue<String> queue = new ArrayBlockingQueue<>(30);
public static void main(String[] args) {
new SaleTicket().asyncSale();
}
private void asyncSale(){
new Thread(() -> {
for (int i = 0; i < 30; i++) {
try {
String saler = queue.take();
database.sale(saler);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "Handler").start();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 40; j++) {
try {
lock.lock();
if(cache.getCount() > 0){
cache.countDesc();
queue.put(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + "卖出票, 剩余" + cache.getCount() + "张票.");
}
} catch (Exception e){
} finally {
lock.unlock();
}
}
}, "Saler-" + i).start();
}
}
}
4 线程安全的集合
当多线程并发的修改/读取集合框架中的数据结构时,会造成数据的混乱;在使用这些数据结构时可以自行进行加锁处理;但是使用juc提供的线程安全的集合更方便,有些线程安全的集合效率也更高;
线程安全的集合主要分为三类:
Vector与HashTable;Collections包装器;java.util.concurrent包下的集合
4.1 早期的线程安全集合
Vector与HashTable给所有public方法都加上了synchronized关键字,这种方式会导致性能下降,在一些不需要同步到场景中,这种强制的显得多余;
4.2 Collections包装器
Collections工具类中提供了相应的包装方法将他们包装成相应的线程安全的集合:
List<E> synchronizedList = Collections.sychronizedList(new ArrayList<E>());
Set<E> sychronizedSet = Collections.sychronizedSet(new HashSet<E>());
Map<K, V> synchronizedMap = Collections.sychronizedMap(new HashMap<K, V>());
Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象同步;
但是在使用这种集合时,应该没有任何线程通过原始非同步的方法去访问集合,例如使用迭代器去访问,此时如果有线程在修改集合,则会抛出ConcurrentModificationException,所以使用for each循环遍历时也会有这个问题;如果必须通过迭代器访问,则需要在进行加锁处理;
下面的程序会抛出并发修改异常
public class ConcurrentCollectionDemo {
public static void main(String[] args) {
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
list.addAll(Arrays.asList(1, 2, 3, 4, 5));
for (int i = 0; i < 50; i++) {
new Thread(() -> {
// 集合迭代时仍然需要手动同步,可能有其他线程修改集合
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
Integer next = iterator.next();
}
}).start();
}
for (int i = 0; i < 100; i++) {
new Thread(() -> {
list.add(10);
}).start();
}
}
}
4.3 java.util.concurrent提供的集合
juc提供了ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentSkipListSet和ConcurrentLinkedQueue等集合,这些集合允许并发的对数据结构进行操作,并且吗没有并发修改异常;但是他们会返回弱一致性迭代器,即迭代器不一定能反映最新的变化;
ConcurrentSkipListMap提供了以下方法:
// 如果键在集合中不存在,则将键值对关联起来,并且返回null;否则返回旧的值且不关联;
V putIfAbsent(K key, V val);
// 如果给定的键值对存在,则删除键值对并返回true,否则返回false
boolean remove(K key, V val);
// 如果给定的键与oldVal关联,则用newVal替换并返回true,否则返回false
boolean replace(K key, V oldVal, V newVal);
juc还提供了CopyOnWriteArrayList和CopyOnWriteArraySet,修改线程会先对底层数组进行复制,而迭代器则引用旧的数组,所以返回的也是弱一致性迭代器;
5 Callable与Future
Callable和Runnable类似,都是封装了异步任务,只是Callable接口的call方法可以返回结果,如果希望一个任务可以返回结果,则应该去实现Callable接口,其定义如下:
public interface Callable<V>{
V call() throws Exception;
}
Future保存了异步计算的结果。其定义如下:
public interface Future<V>{
// 阻塞式获取计算结果
V get() throws Exception;
// 获取计算结果,如果超时未完成则抛出异常
V get(long timeout, TimeUnit unit) throws Exception;
// 如果计算开始且mayInterupt为true,则会中断任务
void cancel(boolean mayInterupt);
boolean isDone();
boolean isCancelled();
}
FutureTask包装器可以将Callable对象转换成Future和Runnable,它同时实现了二者的接口; 使用时将Callable任务传给FutureTask即可;
Callable<Integer> computation = ...;
FutureTask<Integer> task = new FutureTask<>(computation);
new Thread(task).start();
Integer result = task.get();
6 执行器
构建新线程需要一定的时间开销,如果程序中创建了大量生命周期很短的线程,应该使用线程池;线程池中包含许多准备运行的空闲线程,将一个Runnable对象交给线程池,如果有空闲线程就会调用其run方法,当run结束时,线程不会死亡,线程被放回线程池,准备为下一个任务提供服务;另外线程池还可以限制并发线程的数目,防止创建过多线程导致虚拟机崩溃;
6.1 普通线程池
执行器Executor类有许多静态工厂用来创建线程池,其方法如下:
ExecutorService newCachedThreadPool():返回带缓存的线程池,该线程池在必要的时候创建线程,线程空闲60s后被销毁;ExecutorService newFixedThreadPool(int threads):返回一个固定大小的线程池;ExecutorService newSingleThreadPool():创建一个单线程的线程池;
线程池ExecutorService有如下方法:
Future<T> submit(Callable<T> task):提交指定的任务执行;Future<?> submit(Runnable task):提交一个Runnable任务,返回结果为null;Future<T> submit(Runnable task, T result): 提交一个Runnable任务,当任务执行完返回result;void shutdown():关闭线程池,会先完成已经提交的任务再关闭;
下面的例子分别演示了三种submit方法:
public class SubmitDemo {
private static ExecutorService service = Executors.newSingleThreadExecutor();
public static void main(String[] args) {
Future<String> task1 = service.submit(() -> {
System.out.println("Callable task running...");
Thread.sleep(2000);
return "Callable result!";
});
Future<String> task2 = service.submit(() -> {
System.out.println("Runnable task with result running...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "Runnable result!");
Future<?> task3 = service.submit(() -> {
System.out.println("Runnable task no result running...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
try {
System.out.println("Callable任务结果: " + task1.get());
System.out.println("Runnable任务结果: " + task2.get());
System.out.println("无结果的Runnable任务结果: " + task3.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
结果如下:
Callable task running...
Callable任务结果: Callable result!
Runnable task with result running...
Runnable任务结果: Runnable result!
Runnable task no result running...
无结果的Runnable任务结果: null
6.2 预定执行线程池
执行器Executors还有两个方法可以创建预定任务的线程池ScheduledExecutorService,允许预定任务在延迟一定时间后执行一次,也可以预定任务在指定时间后周期性的执行;执行器的两个创建预定任务的线程池方法如下:
ScheduledExecutorService newScheduledThreadPool(int thread)ScheduledExecutorService newSingleThreadScheduledExecutor()
预定义任务线程池ScheduledExecutorService除了普通线程池的方法外,还有有如下两组方法来指派任务:
6.2.1 预定义在指定时间后执行任务
ScheduledFuture<V> schedule(Callable<V> task, long time, TimeUnit unit);
ScheduledFuture<?> schedule(Runnable task, long time, TimeUnit unit);
6.2.2 预定义指定时间后周期性执行任务
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit)方法预定义在初始延迟时间后,周期性的运行给定的任务;如果任务时间比周期时间久,则周期时间为任务运行时间;
下面的程序任务时间为5s,而周期时间为3s,则两次任务之间间隔时间为5s;
public class ScheduledTask {
private static ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
public static void main(String[] args) {
service.scheduleAtFixedRate(() -> {
try {
System.out.println("周期性执行的任务开始,当前时间: " + new SimpleDateFormat("yyyy年MM月dd日-hh:mm:ss").format(new Date()));
Thread.sleep(5000);
System.out.println("周期性执行的任务完毕,当前时间: " + new SimpleDateFormat("yyyy年MM月dd日-hh:mm:ss").format(new Date()));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, 2, 3, TimeUnit.SECONDS);
}
}
执行结果如下:两次任务上一次任务结束下一次任务立马开始,两次任务开始时间之间的间隔为5s(任务执行时间);
周期性执行的任务开始,当前时间: 2023年10月16日-11:13:54
周期性执行的任务完毕,当前时间: 2023年10月16日-11:13:59
周期性执行的任务开始,当前时间: 2023年10月16日-11:13:59
周期性执行的任务完毕,当前时间: 2023年10月16日-11:14:04
周期性执行的任务开始,当前时间: 2023年10月16日-11:14:04
周期性执行的任务完毕,当前时间: 2023年10月16日-11:14:09
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit)方法预定义在初始延迟时间后,上一次任务结束到下一次任务开始的时间间隔为delay;
public class ScheduledTask {
private static ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
public static void main(String[] args) {
service.scheduleWithFixedDelay(() -> {
try {
System.out.println("周期性执行的任务开始,当前时间: " + new SimpleDateFormat("yyyy年MM月dd日-hh:mm:ss").format(new Date()));
Thread.sleep(5000);
System.out.println("周期性执行的任务完毕,当前时间: " + new SimpleDateFormat("yyyy年MM月dd日-hh:mm:ss").format(new Date()));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, 2, 3, TimeUnit.SECONDS);
}
}
执行结果如下:两次任务之间的时间间隔为3s
周期性执行的任务开始,当前时间: 2023年10月16日-11:11:49
周期性执行的任务完毕,当前时间: 2023年10月16日-11:11:54
周期性执行的任务开始,当前时间: 2023年10月16日-11:11:57
周期性执行的任务完毕,当前时间: 2023年10月16日-11:12:02
周期性执行的任务开始,当前时间: 2023年10月16日-11:12:05
周期性执行的任务完毕,当前时间: 2023年10月16日-11:12:10
6.3 控制任务组
线程池还可以控制一组任务的执行,比如如果一组任务中某个任务完成,则所有任务结束;比如查找任务,可以将数据划分成n组,每一组执行一个查询任务,当某一个查询到,则所有任务结束。或者所有任务都完成才返回一组结果集;
ExecutorService有如下两组方法来控制一组任务:
6.3.1 invokeAny
该方法有两种定义, 第二种带超时时间,当超过指定时间未返回结果,会抛出TimeoutException
T invokeAny(Collection<Callable<T>> tasks);
T invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit);
下面例子为查找一组数据,为了模拟任务拆分的好处,假设每次查询耗时2ms,将任务划分为在不同区间进行查找,只要有一个任务返回结果,则所有任务结束;
首先定义查询任务
class SearchTask<T> implements Callable<Integer> {
public static final int SINGLE_CONSUME = 2;
private T[] arr;
private int start;
private int end;
private T target;
public SearchTask(T[] arr, int start, int end, T target){
this.arr = arr;
this.start = start;
this.end = end;
this.target = target;
}
@Override
public Integer call() throws Exception {
for (int i = start; i < end; i++) {
Thread.sleep(SINGLE_CONSUME);
if(arr[i].equals(target)) {
return i;
}
}
// 如果此任务没有查询到,应该等待,不然其他任务也会结束
Thread.sleep(30, 000);
return null;
}
}
__使用invokeAny__完成查询
public class InvokeDemo {
private static ExecutorService service = Executors.newFixedThreadPool(10);
private static final int LENGTH = 10000;
private static final int TASK_LENGTH = 2000;
private static Random random = new Random();
private static Integer[] arr;
static {
Set<Integer> set = new HashSet<>();
arr = new Integer[LENGTH];
for (int i = 0; i < LENGTH; i++) {
int num = random.nextInt(LENGTH);
// 保证arr中元素不重复
while (set.contains(num))
num = random.nextInt(LENGTH);
arr[i] = num;
set.add(num);
}
}
public static void main(String[] args) {
testInvokeAny();
}
private static void testInvokeAny(){
List<SearchTask<Integer>> tasks = new ArrayList<>();
int start = 0;
int idx = random.nextInt(6000) + 4000;
for (int i = 0; i < LENGTH / TASK_LENGTH; i++) {
tasks.add(new SearchTask<>(arr, start, start + TASK_LENGTH, arr[idx]));
start += TASK_LENGTH;
}
try {
long startTime = System.currentTimeMillis();
Integer result = service.invokeAny(tasks);
System.out.println("找到,下标为:" + result + "耗时:" + (System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < LENGTH; i++) {
Thread.sleep(Thread.sleep(SearchTask.SINGLE_CONSUME););
if(arr[i].equals(arr[idx])) {
System.out.println("找到,下标为:" + i + "耗时:" + (System.currentTimeMillis() - startTime));
break;
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} finally {
service.shutdown();
}
}
}
结果如下:任务划分之后,查询效率提升很多;
找到,下标为:4213耗时:634
找到,下标为:4213耗时:11384
6.3.2 invokeAll
该方法会返回一组Future,有两种定义, 第二种带超时时间,当超过指定时间未返回结果,会抛出TimeoutException;
该方法只有当所有线程都工作完毕才会返回结果,因此如果有某个任务十分耗时,则所有任务都会等待;
List<Future<T>> invokeAll(Collection<Callable<T>> tasks);
List<Future<T>> invokeAll(Collection<Callable<T>> tasks, long timeout, TimeUnit unit);
下面的例子为对一个数组求和,可以对任务进行划分,每个人物在不同区间求和,为了模拟任务划分的效果,假设每次求和操作耗时2ms;
首先定义求和任务
class AddTask implements Callable<Integer> {
public static final int SINGLE_CONSUME = 2;
private Integer[] arr;
private int start;
private int end;
public AddTask(Integer[] arr, int start, int end){
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = start; i < end; i++) {
result += arr[i];
Thread.sleep(SINGLE_CONSUME);
}
return result;
}
}
使用invokeAll进行求和
public class InvokeDemo {
private static ExecutorService service = Executors.newFixedThreadPool(10);
private static final int LENGTH = 10000;
private static final int TASK_LENGTH = 2000;
private static Random random = new Random();
private static Integer[] arr;
static {
arr = new Integer[LENGTH];
for (int i = 0; i < LENGTH; i++) {
arr[i] = random.nextInt(10);
}
}
public static void main(String[] args) {
testInvokeAll();
}
private static void testInvokeAll(){
List<AddTask> tasks = new ArrayList<>();
int startIdx = 0;
for (int i = 0; i < LENGTH / TASK_LENGTH; i++) {
tasks.add(new AddTask(arr, startIdx, startIdx + TASK_LENGTH));
startIdx += TASK_LENGTH;
}
try {
long startTime = System.currentTimeMillis();
List<Future<Integer>> futures = service.invokeAll(tasks);
int result = 0;
for (Future<Integer> future : futures) {
result += future.get();
}
System.out.println("求和结果为: " + result + ", 耗时:" + (System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
result = 0;
for (int i = 0; i < LENGTH; i++) {
result += arr[i];
Thread.sleep(AddTask.SINGLE_CONSUME);
}
System.out.println("求和结果为: " + result + ", 耗时:" + (System.currentTimeMillis() - startTime));
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} finally {
service.shutdown();
}
}
}
6.3.3 CompletionService
可以使用线程池构造CompletionService来控制一组任务,一般使用new ExecutorCompletionService(executor)来构建;
ExecutorCompletionService(Executor e);
ExecutorCompletionService内部维护了一个Future对象的阻塞队列,其中包含了已经提交的任务结果;这种方式控制一组任务有两点好处:
- 不会产生
invokeAll因为某个任务时间过久而导致迟迟无法返回结果的问题; - 相比于直接使用
submit返回的Future去自行控制一组任务的这种方式提供了阻塞队列来自行管理;
CompletionService接口定义如下:submit和普通的线程池一样,take是阻塞式的取任务结果,而poll当队列没有元素会返回null;
public interface CompletionService<V>{
Future<V> submit(Callable<V> task);
Future<V> submit(Runnable task, V result);
Future<V> take() throws InterruptedException;
Future<V> poll();
Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}
下面的例子有五个任务并发的执行,其中一个任务十分耗时,演示相较于invokeAll方法的优势;
private static void testCompletionService() {
ExecutorService service = Executors.newFixedThreadPool(5);
CompletionService<String> completionService = new ExecutorCompletionService<>(service);
for (int i = 0; i < 4; i++) {
completionService.submit(() -> {
String time = new SimpleDateFormat("yyyy年MM月dd日-hh:mm:ss").format(new Date());
System.out.println(time + "\t" + Thread.currentThread().getName() + " running...");
Thread.sleep(new Random().nextInt(4000) + 1000);
return Thread.currentThread().getName();
});
}
completionService.submit(() -> {
String time = new SimpleDateFormat("yyyy年MM月dd日-hh:mm:ss").format(new Date());
System.out.println(time + "\t" + Thread.currentThread().getName() + " running...");
Thread.sleep(20000);
return Thread.currentThread().getName() + "-special";
});
try {
for (int i = 0; i < 5; i++) {
Future<String> result = completionService.take();
String time = new SimpleDateFormat("yyyy年MM月dd日-hh:mm:ss").format(new Date());
System.out.println(time + "\t" + result.get() + " completed!");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} finally {
service.shutdown();
}
}
6.4 Fork-Join框架
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。这里是真正的并行执行,所有是在多核的CPU上执行,每个核单独一个线程。ForkJoin框架为我们提供了RecursiveAction和RecursiveTask<T>,它们都继承自ForkjoinTask;我们只需要继承这两个抽象类的其中一个并且实现compute方法,然后用ForkJoinPool的invoke方法执行任务就可以自动的划分任务;
ForkJoinTask<V>提供了如下方法:
// ForkJoinTask对象调用时会将当前任务加入任务队列执行
public final ForkJoinTask<V> fork();
// 执行给定的任务
public static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2);
public static void invokeAll(ForkJoinTask<?>... tasks);
// 当任务完成时返回compute方法计算返回的结果
public final V join();
用户需要做的就是去继承RecursiveAction或RecursiveTask<T>并实现compute方法;如果任务没有返回值,则继承RecursiveAction并实现void compute();如果任务有返回值,则继承RecursiveTask<T>实现V compute();
6.4.1 RecursiveAction案例
用RecursiveAction来实现并行计算的二分归并排序算法
定义任务类
在compute方法中,根据阈值来确认任务是否继续划分,此处选100w作为阈值;当数组长度小于阈值,直接使用二分归并排序;当任务大于阈值,则将任务一分为二,并行的去执行子任务;
class MergeSortTask extends RecursiveAction {
private static final int THRESHOLD = 1000000;
public static final int MAX = 10000;
private int[] arr;
private int left;
private int right;
public MergeSortTask(int[] arr, int left, int right){
this.arr = arr;
this.left = left;
this.right = right;
}
@Override
protected void compute() {
if(right - left <= THRESHOLD){
mergeSort(arr, left, right);
} else {
int mid = (left + right) / 2;
MergeSortTask task1 = new MergeSortTask(arr, left, mid);
MergeSortTask task2 = new MergeSortTask(arr, mid, right);
invokeAll(Arrays.asList(task1, task2));
merge(arr, left, mid, right);
}
}
// 二分归并算法
public static void mergeSort(int[] arr, int left, int right) {
if(right - left <= 1) {
return;
}
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid, right);
merge(arr, left, mid, right);
}
private static void merge(int[] data, int l, int mid, int r){
int n_l = mid - l;
int n_r = r - mid;
int[] left = new int[n_l + 1];
int[] right = new int[n_r + 1];
int i = 0, j;
for(; i < n_l; i++)
left[i] = data[l + i];
left[i] = MAX;
for(i = 0; i < n_r; i++)
right[i] = data[mid + i];
right[i] = MAX;
i = j = 0;
for(int k = l; k < r; k++){
if(left[i] <= right[j])
data[k] = left[i++];
else
data[k] = right[j++];
}
}
}
测试类
这里使用10亿个数据来测试并行的二分归并算法和单线程二分归并的速度
public class ForkJoinMergeSort {
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
int count = 100000000;
int[] data = new int[count];
Random random = new Random();
for (int i = 0; i < count; i++) {
data[i] = random.nextInt(1000);
}
int[] dataBck = Arrays.copyOf(data, count);
MergeSortTask task = new MergeSortTask(data, 0, count);
long start = System.currentTimeMillis();
pool.invoke(task);
System.out.println("并行任务耗时: " + (System.currentTimeMillis() - start));
start = System.currentTimeMillis();
MergeSortTask.mergeSort(dataBck, 0, count);
System.out.println("单线程任务耗时: " + (System.currentTimeMillis() - start));
}
}
结果如下: 并行计算的速度要远远优于单线程的速度;
并行任务耗时: 3806
单线程任务耗时: 13477
6.4.2 RecursiveTask案例
现在有一个数组需要统计其中满足条件的元素个数,先定义一个Filter接口,其accept方法用于定义需要满足的条件;
interface Filter{
boolean accept(double v);
}
定义统计任务的类
此任务去实现RecursiveTask<V>的V compute()方法;也是根据阈值来确定是否继续划分子任务,当小于子任务时,直接遍历数组,判断是否满足条件,满足则计数加1;如果大于阈值则划分子任务,然后调用invokeAll来并行执行所有子任务,最后调用ForkJoinTask::join来汇总子任务的结果并返回;(注意,使用Thread.sleep来模拟任务耗时);
class CounterTask extends RecursiveTask<Integer> {
private static final int THRESHHOLD = 1000;
private double[] arr;
private int from;
private int to;
private Filter filter;
public CounterTask(double[] arr, int from, int to, Filter filter){
this.arr = arr;
this.from = from;
this.to = to;
this.filter = filter;
}
@Override
protected Integer compute() {
if (to - from < THRESHHOLD){
int count = 0;
for (int i = from; i < to; i++) {
if(filter.accept(arr[i])) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
count++;
}
return count;
} else {
int mid = (from + to) / 2;
CounterTask first = new CounterTask(arr, from, mid, filter);
CounterTask second = new CounterTask(arr, mid, to, filter);
// 另一种写法
// invokeAll(first, second);
// return first.join() + second.join();
return invokeAll(Arrays.asList(first, second))
.stream()
.mapToInt(ForkJoinTask::join)
.sum();
}
}
}
测试类
public class ForkJoinCounter {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
int count = 10000;
double[] arr = new double[count];
for (int i = 0; i < count; i++)
arr[i] = Math.random();
CounterTask task = new CounterTask(arr, 0, arr.length, x -> x > 0.5);
int countAcc = 0;
long start = System.currentTimeMillis();
for (int i = 0; i < arr.length; i++) {
if(arr[i] > 0.5){
countAcc++;
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
long end = System.currentTimeMillis();
System.out.println("countAcc = " + countAcc + ", 非并行任务耗时:" + (end - start) + "ms");
start = System.currentTimeMillis();
pool.invoke(task);
end = System.currentTimeMillis();
System.out.println("countAcc = " + countAcc + ", 并行任务耗时:" + (end - start) + "ms");
}
}
运行结果:
countAcc = 5143, 非并行任务耗时:39562ms
countAcc = 10000, 并行任务耗时:3567ms
7 同步器
juc提供了比 synchronized 更加高级的各种同步结构,如果有相互合作的工作线程集满足这些模式之一,应该直接使用对应的同步器,而不要通过手动去加锁来控制线程的同步;
7.1 信号量Semaphore
信号量通过控制一定数量的许可证来达到控制一个线程集,一个线程集只有获得许可的线程才能访问临界资源;Semaphore的构造函数需要传入整型变量的数目,表示许可证的数量;
-
当调用
acquire()方法时,如果有剩余的许可证,则线程进入临界区,否则线程阻塞; -
当调用
release()方法时,释放一个许可证;
下面的程序定义了一个有两个许可证的信号量,因此只能有两个线程可以同时进入临界区;
public static void testSemaphore() {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
String threadName = Thread.currentThread().getName();
semaphore.acquire();
System.out.println(threadName + " 开始工作");
int sleepMills = random.nextInt(10000) + 5000;
Thread.sleep(sleepMills);
System.out.printf("%f 工作时间%dms, 释放资源!", threadName, sleepMills);
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
7.2 倒计时门栓CountDownLatch
一个CountDownLatch让一个线程集等待直至计数变为0;需要注意的是,CountDownLatch对象是一次性的,当其计数变为0时,就不可再用;
它的构造函数需要传入一个整型变量,表示计数器的初始值;
然后当调用countDown()时,计数器减1;调用await()方法时,当前线程会等待直至计数器为0;
public static void testCountDownLatch() {
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 开始工作");
int sleepMills = random.nextInt(5000) + 2000;
Thread.sleep(sleepMills);
System.out.println(Thread.currentThread().getName() + " 工作时间" + sleepMills + "ms!");
latch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
try {
latch.await();
System.out.println("主线程工作");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
7.3 循环障栅CyclicBarrier
通过CyclicBarrier可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行;和CountDownLatch不同的是,CyclicBarrier可以被重用。
构造函数两种:
第二个构造函数指定当一定数目的线程到达屏障点之后执行barrierAction中定义的线程,然后线程集中的所有线程再继续执行;第二个构造函数;
public CyclicBarrier(int parties);
public CyclicBarrier(int parties, Runnable barrierAction);
当调用CyclicBarrier::await()方法时,线程阻塞,直至有指定数目的线程调用await时才解除阻塞继续执行;
下面的程序模拟当3个人准备好后开始比赛的效果,可以看出CyclicBarrier是可以重复使用的;
private static void testCyclicBarrier() {
CyclicBarrier cb = new CyclicBarrier(3, () -> {
System.out.println("所有人员准备完毕,开始比赛~ ");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
String threadName = Thread.currentThread().getName();
for (int j = 0; j < 5; j++) {
int sleepMills = random.nextInt(10000) + 5000;
Thread.sleep(sleepMills);
System.out.println(threadName + "准备就绪, 准备时间" + sleepMills + "ms.");
cb.await();
System.out.println(threadName + "继续");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
}
7.4 交换器Exchanger
交换器和CyclicBarrier有点类似,当两个线程都达到屏障点时,可以继续执行;与之不同的是,交换器允许两个线程交换数据;
交换器是一个泛型类,泛型为交换的数据的类型,它只有一个无参构造函数,当调用V exchange(V data)时,线程阻塞,直至有另一个线程也调用此方法,才继续执行并获得交换的数据;
值得注意的是,如果有两个以上的线程,则交换数据的配对是随机的,哪两个线程先调用exchange方法则交换数据;
public static void testExchanger() {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
String data1 = exchanger.exchange("data1");
System.out.println(Thread.currentThread().getName() + "获得数据: " + data1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "AA").start();
new Thread(() -> {
try {
Thread.sleep(3000);
String data1 = exchanger.exchange("data2");
System.out.println(Thread.currentThread().getName() + "获得数据: " + data1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "BB").start();
}
7.5 同步队列SynchronousQueue
同步队列将生产者与消费者配对,当一个线程调用put方法时,它会阻塞直到另一个线程调用take方法;与交换器不同的是,他只允许数据从生产者向消费者传递;
public static void testSynchronousQueue() {
SynchronousQueue<String> queue = new SynchronousQueue<>();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put("data " + i);
System.out.println(Thread.currentThread().getName() + "放入: data " + i);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "AA").start();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(10000);
String data = queue.take();
System.out.println(Thread.currentThread().getName() + "获得数据: " + data);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "BB").start();
new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(5000);
String data = queue.take();
System.out.println(Thread.currentThread().getName() + "获得数据: " + data);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "CC").start();
}
浙公网安备 33010602011771号