前言
来啦!来啦!终于来啦!上一篇链接:Java 多线程和并发编程(一)——附代码,欢迎小伙伴们阅读并指正。
并发计算
我们现在计算机所面临的业务特点是任务多,数据量大,传统的串行编程如顺序、选择、循环等结构已经不能满足我们的需求,所以这要求我们尽可能的采用并行编程即多线程。
然而并行编程存在一定的困难,比如任务分配和执行过程高度耦合,所以我们应该如何控制粒度来切割任务?又该如何分配任务给线程,并监督线程的执行过程。接下来的讲解中会告诉答案
并行模式
并行模式有两种:
主从模式(Master- Slave)
有一个主线程,剩下都是从线程,主线程指挥从线程,主线程起到一个协调的作用如 main 线程,相对简单
Worker模式(Worker to Worker)
也叫做(Peer to Peer)/ (p2p) ,在这种模式下大家都是平等的,工作内容都是一样的
并发编程
Java的并发编程主要有三种: Thread/Runnable/Thread 组管理、Executor 并发框架、Fork -Join 框架
Thread/Runnable/Thread 组管理
在上一篇文章中,我们在线程的创建处已经描述了Thread/Runnable,而当线程很多的时候,我们则需要一个线程组来方便我们的管理。
ThreadGroup
- 是线程的集合,一种树形结构,大线程组可以包括小线程组;
- 可以通过 enumerate 的方法遍历组内的线程,执行操作;
- 能够有效的管理多个线程,但是管理效率低,属于低层次管理;
- 仍然存在任务分配和执行过程高度耦合问题;
- 存在重复创建线程、关闭线程操作,无法重用线程,因为线程和线程组内的线程,都是new出来的,但是start一次以后,就不能再次start,new的代价很昂贵,只运行一次,性价比过低;
- 在默认情况下,子线程和创建它的父线程处于同一个线程组内。
public class ThreadGroupExample {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("MyThreadGroup"); // 创建线程组
// 创建线程并将它们添加到线程组中
Thread thread1 = new Thread(group, new MyRunnable(), "Thread 1");
Thread thread2 = new Thread(group, new MyRunnable(), "Thread 2");
Thread thread3 = new Thread(group, new MyRunnable(), "Thread 3");
// 启动线程
thread1.start();
thread2.start();
thread3.start();
// 输出线程组中的线程信息
group.list();
// 等待线程组中的所有线程执行完成
while (group.activeCount() > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 中断线程组中的所有线程
group.interrupt();
}
static class MyRunnable implements Runnable {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 线程中断异常处理
}
}
}
}
Executor
要先理解 Executor 框架,需要先了解共享线性池的概念,Java共享线程池(Shared Thread Pool)是一种基于线程池的并发编程机制,它可以在多个任务之间共享一组线程来执行任务。Java提供了 Executor 框架来支持线程池的创建和管理。
- 预设好的多个Thread,可弹性增加
- 多次执行很多很小的任务
- 任务创建和执行过程是解耦的,即创建好一个任务,丢给线程池去执行,线程池执行的任务,我们可以不用去关心
- 程序员无需关心线性池执行任务过程
Java 从JDK 5 开始提供 Executor Frame Work ,负责管理多个异步任务的执行,而无需程序员显示地管理线程的生命周期。Executor 提供了一些预定义的线程池实现:ExecutorService,ThreadPoolExecutor,Future等。
- Executors.newCachedThreadPool/newFixedThreadPool:创建线程池
- ExecutorService:线程池服务
- Callable:具体的逻辑对象(线程类),和Runnable是等价的,可以用来执行一个任务,Runnable的run方法没有返回值,而Callable的call方法可以有返回值
- Future:返回结果(包含多线程执行完了以后存储的结果)
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
// 线程池的两种创建方式
// 括号里面不带参,这个时候线程池的量会随着任务的量自动增长
// 括号里面带参数,创建一个固定大小为 3 的线程池
Executor executor = Executors.newFixedThreadPool(3);
// 提交任务到线程池
executor.execute(new RunnableTask("Task 1"));
executor.execute(new RunnableTask("Task 2"));
executor.execute(new RunnableTask("Task 3"));
}
static class RunnableTask implements Runnable {
private final String name;
public RunnableTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("Executing task: " + name);
// 这里可以根据任务的具体逻辑执行相应的操作
}
}
}
一般来说,我们不希望线程池的数目太多,太多的话也会影响计算机的性能,通常设定线程数是CPU核的2倍或者4倍,尽量采用Executor框架来实现多并发。
Fork -Join
Java 提供另一种并行框架:分解、治理、合并(分治编程),即通过将大任务分割成小任务来提高并发效率,适用于整体任务量不好确定的场合(最小任务可确定)。提供的关键实现类有:ForkJoinPool 任务池、Recursive Action、RecuresiveTask等
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ForkJoinExample {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
// 创建一个计算任务
RecursiveTask<Integer> task = new RecursiveTask<Integer>() {
//compute()方法中,对任务进行划分,将大任务拆分为多个小任务
@Override
protected Integer compute() {
if (taskSize <= threshold) {
return computeDirectly();
} else {
// 将任务分割为子任务并使用join()方法等待子任务完成并获取结果
RecursiveTask<Integer> subtask1 = new RecursiveTask<Integer>() {
@Override
protected Integer compute() {
// 子任务1的逻辑
}
};
RecursiveTask<Integer> subtask2 = new RecursiveTask<Integer>() {
@Override
protected Integer compute() {
// 子任务2的逻辑
}
};
//使用fork()方法将子任务提交到线程池中执行
subtask1.fork();
subtask2.fork();
int result1 = subtask1.join();
int result2 = subtask2.join();
// 对子任务的结果进行合并并返回最终结果
return combineResults(result1, result2);
}
}
};
// 提交任务到Fork-Join框架
int result = forkJoinPool.invoke(task);
System.out.println("Result: " + result);
}
}
Java 并发数据结构
Java 并发集合框架
- 阻塞式集合:当集合为空或者满时,等待
- 非阻塞式集合:当集合为空或者满时,不等待,返回 null 或异常
什么是线程安全?
所谓的线程安全,是指在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
而常用的数据结构是线程不安全的:如ArrayList,HashMap,HashSet 是非同步的,多个线程同时读写,可能会抛出异常或数据错误;传统Vector,Hashtable 等同步集合性能过差,下面我们来详细介绍一下:
List
- Vector 同步安全,写多读少
- ArrayList 不安全
- Collections.synchronizedList(List list )用这种方法可以把线程不安全的编程一个线程安全的,基于synchronized(上一篇中讲互斥的时候我们已经讲过,被它所包围的代码,一次只能由一个线程来执行),效率差
- CopyOnWriteArrayList 读多写少,基于复制机制,非阻塞
import java.util.concurrent.CopyOnWriteArrayList;
CopyOnWriteArrayList<String> concurrentList = new CopyOnWriteArrayList<>();
concurrentList.add("item1");
concurrentList.add("item2");
concurrentList.add("item3");
String item = concurrentList.get(0);
concurrentList.remove("item2");
Set
- HashSet 不安全
- Collections.synchronizedSet(Set set )基于synchronized,效率差
- CopyOnWriteArraySet (基于CopyOnWriteArrayList实现)读多写少,非阻塞
Map
- Hashtable 同步安全,写多读少
- HashMap 不安全
- Collections.synchronizedMap(Map map )基于synchronized,效率差
- ConcurrentHashMap 读多写少,非阻塞
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("key1", 1);
concurrentHashMap.put("key2", 2);
concurrentHashMap.put("key3", 3);
int value = concurrentHashMap.get("key1");
concurrentHashMap.remove("key2");
Queue & Deque
Queue:队列,Deque:双向队列,这是在JDK 1.5提出的
- ConcurrentLinkedQueue 非阻塞
- ArrayBlockingQueue/ LinkedBlockingQueue 阻塞队列
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);
blockingQueue.put("item1");
blockingQueue.put("item2");
blockingQueue.put("item3");
String item = blockingQueue.take();
boolean isRemoved = blockingQueue.remove("item2");
细粒度的线程控制和协作
线程协作
回顾一下前面谈到的三种并发编程:Thread /Executor/Fork-Join,他们的执行过程都是线程启动,运行,结束,线程之间缺少协作,Java中提供了下面几种方式来增强线程协作。
synchronized 同步
限定只有一个线程才能进入关键区,基于 JVM 实现的,功能较为简单,性能损失较大。
synchronized (obj) {
// 执行临界区代码
}
Lock(synchronized 的升级版)
Lock可以实现同步的效果,能够实现更复杂的临界区结构。
- tryLock 方法可以预判锁是否空闲,允许分离读写的操作,多个读,一个写(读时共享的,写是排他的),性能更好
- ReentrantLock类 ,可重入的互斥锁
- ReentrantReadWriteLock类,可重入的读写锁
- lock和unlock函数
Lock lock = new ReentrantLock();
lock.lock();
try {
// 执行临界区代码
} finally {
lock.unlock();
}
Semaphore
信号量:本质上是一个计数器,计数器 > 0,可以使用,= 0,不能使用。比Lock更进一步,用于控制多个线程对共享资源的访问,可以设置最大访问线程数。
- acquire 获取
- release 释放
import java.util.concurrent.Semaphore;
Semaphore semaphore = new Semaphore(3); // 设置信号量的许可数为3
// 线程1
new Thread(() -> {
try {
semaphore.acquire(); // 获取一个信号量许可
// 执行一些操作
semaphore.release(); // 释放一个信号量许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 线程2
new Thread(() -> {
try {
semaphore.acquire(); // 获取一个信号量许可
// 执行一些操作
semaphore.release(); // 释放一个信号量许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Latch
等待锁,是一个同步辅助类,用来同步执行任务的一个或者多个线程。它和上面提到的不同,不是保护临界区或者共享资源,而是协调各个线程执行到某个地方的时候让大家都暂停。
主要实现类:
CountDownLatch(向下计数的一个类)
countDown () 计数减一
await()等待latch变为0,如果latch 不为0 ,继续等待
import java.util.concurrent.CountDownLatch;
CountDownLatch latch = new CountDownLatch(3);
// 线程1
new Thread(() -> {
// 做一些操作
latch.countDown();
}).start();
// 线程2
new Thread(() -> {
// 做一些操作
latch.countDown();
}).start();
// 线程3
new Thread(() -> {
// 做一些操作
latch.countDown();
}).start();
try {
latch.await(); // 等待倒计时门闩计数归零
} catch (InterruptedException e) {
e.printStackTrace();
}
Barrier
是一个集合点,也是一个同步辅助类,允许多个线程在某一个点上进行同步
主要实现类:
- CyclicBarrier
- 构造函数是需要同步的线程数量
- await 等待其他线程,到达数量后,就放行
CyclicBarrier barrier = new CyclicBarrier(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
// 执行任务
barrier.await();
// 与 Latch 不同,每个线程到达屏障后可以继续执行接下来的任务
}).start();
}
Phaser
允许执行并发阶段任务,同步辅助类。在每个阶段结束的位置对线程进行同步,当所有的线程都到达这步,再进行下一步,和前面barrier不同的是它可以多次用来应用。且提供更高级的功能,可以动态地添加和删除参与线程,同时还可以分阶段执行任务。
主要的实现类:
- Phaser
- arrive()
- arriveAndAwaitAdvance()
Phaser phaser = new Phaser(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
// 执行任务
int phase = phaser.arriveAndAwaitAdvance();
// 与 CyclicBarrier 不同,Phaser 支持分阶段执行
}).start();
}
Exchanger
不做线程协调,而是在并发线程中互相交换信息。允许在2个线程中定义同步点,当两个线程都同时执行到同一个exchanger的exchange方法,才进行数据交换(双向交换),主要的方法是exchange()。注意,它仍做不到像MPI那样可以点对点的发送信息。
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
String data1 = "data1";
String data2 = exchanger.exchange(data1);
// 收到对方的数据,执行后续操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 另一个线程同理
多线程的应用
计时任务
我们前面谈到的三种并发编程,都是立即执行,假设现在需要增加定时执行的任务,该怎么做呢?有下面三种方式:
简单定时器机制
- 设置计划任务,在指定的时间开始执行某一个任务;
- 利用 TimerTask 封装任务;
- Timer类定时器
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class SimpleTimerExample {
public static void main(String[] args) {
Timer timer = new Timer();
// 创建一个延迟1秒执行的任务
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("任务执行时间:" + new Date());
}
};
// 在当前时间的基础上延迟1秒后执行任务,并且每间隔2秒重复执行一次
timer.schedule(task, 1000, 2000);
}
}
Executor + 定时器机制
Executor 中定时器的线程池实现类:ScheduledExecutorService,可以帮我们实现定时任务,周期任务
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ExecutorTimerExample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
// 创建一个延迟1秒执行的任务
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("任务执行时间:" + new Date());
}
};
// 在当前时间的基础上延迟1秒后执行任务,并且每间隔2秒重复执行一次
executor.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS);
}
}
第三方库:Quartz
这是一个较为完善的任务调度框架,可以解决程序中Timer零散管理的问题,其中:
Timer 执行周期任务,如果中间某一次有一场,整个任务终止执行;
Quartz执行周期任务,如果中间某一次有异常,不影响下次任务执行。
写在后面
感觉还有好多内容,留到以后再慢慢添加吧~~~