并行 & 并发
- 发(concurrency)和并行(parallellism)
- 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
- 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
线程&多线程&进程
a、线程:
线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
b、多线程:
多线程指在单个程序中可以同时运行多个不同的线程执行不同的任务。多线程编程的目的,其实就是“最大限度地利用 cpu 资源”,当某一线程的处理不需要占用 cpu 而只和 io 等资源打交道时,让需要占用 Cpu 的其他线程有其他机会获得 cpu 资源。从根本上说,这就是多线程编程的最终目的。
c、线程与进程的区别
- 进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。
- 一个程序至少有一个进程,一个进程至少有一个线程。
线程的实现方式
- 继承Thread类,重写run方法
public class ThreadDemo extends Thread {
public static void main(String[] args) {
ThreadDemo thread = new ThreadDemo();
thread.start();
}
@Override
public void run() {
System.out.println("山川异域,风月同天");
}
}
- 实现Runnable接口,重写run方法·
public class RunnableDemo implements Runnable{
public static void main(String[] args) {
new Thread(new RunnableDemo()).start();
}
@Override
public void run() {
System.out.println("山川异域,风月同舟");
}
}
- 通过Callable和FutureTask创建线程,并实现其call()方法
//实现Callable 重写call方法 可带参数返回
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
return "山川异域";
}
public static void main(String[] args) throws InterruptedException,ExecutionException {
FutureTask task = new FutureTask(new CallableDemo());
new Thread(task).start();
System.out.println(task.get().toString());
}
}
三种实现线程的区别
实现 Runnable 接口相比继承 Thread 类有如下优势
- 可以避免由于 Java 的单继承特性而带来的局限
- 增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
- 线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类
实现 Runnable 接口和实现 Callable 接口的区别 - Runnable 是自从 java1.1 就有了,而 Callable 是 1.5 之后才加上去的
- 实现 Callable 接口的任务线程能返回执行结果,而实现 Runnable 接口的任务线程不能返回结果
- Callable 接口的 call()方法允许抛出异常,而 Runnable 接口的 run()方法的异常只能在内部消化,不能继续上抛
- 加入线程池运行,Runnable 使用 ExecutorService 的 execute 方法,Callable 使用 submit 方法
注: Callable 接口支持返回执行结果,此时需要调用 FutureTask.get()方法实现,此方法会阻塞主线程直到获取返回结果,当不调用此方法时,主线程不会阻塞
线程的生命周期&状态
状态 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,JAVA线程将操作系统中的就绪和运行两种状态统称为’运行中’ |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
- idea调试技巧
断点打在某行(此处选择线程走完run方法前),进入控制台找Debugger,Frames下拉框内进行不同线程的切换,选中Frames中的Thread类,使用Evaluate Expression,输入this.State()查看当前线程的状态。
tips 查看线程状态:
jps -> jstack pid (常用于判断死锁)
tips 避免死锁的方法:
·避免一个线程同时获取多个锁。
·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
线程的执行顺序
多个线程都是并行执行的,顺序随机,等待CPU调配
tips: join()可以保证让一个线程在另一个线程之前执行结束。
public class JoinThread {
// * join()可以保证让一个线程在另一个线程之前执行结束。
// * 如何保证一个工作在另一个工作结束之前完成,就可以使用join()方法。
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(
new Runnable() {
@Override
public void run() {
try {
System.out.println("线程1开始执行");
Thread.sleep(3000);
System.out.println("线程1结束执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程2开始运行");
thread1.join();// 线程2运行到这里会等待线程1运行结束
System.out.println("线程2结束执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 先开启线程2,在开启线程1
thread2.start();
thread1.start();
// 线程2先启动,当运行到thread1.join()时,线程2停止运行,
// 等待线程1执行结束,虽然线程1启动比线程2迟,但是只有当线程1运行结束后,线程2才能继续运行。
}
}
执行结果
线程2开始运行
线程1开始执行
线程1结束执行
线程2结束执行
线程与线程池对比
需求:使用多线程以最快的速度向list容器插入1万个随机数
普通线程插入数据
既然要最快,那直接开1万个线程,每一个线程只负责插入一个数组那不是一下子就完成了?但是效果并不是想象中那么块
public static void main(String[] args) throws InterruptedException {
//记录开始时间
Long start = System.currentTimeMillis();
final List<Integer> list = new ArrayList<>();
final Random random = new Random();
//循环add随机数入容器
for (int i=0;i<10000;i++){
//每次插入开一个线程
Thread thread = new Thread(){
public void run(){list.add(random.nextInt());}
};
thread.start();
//需要等待所有进程结束才能结束main进程不然会出现数据长度对不上
thread.join();
}
System.out.println("耗时:"+(System.currentTimeMillis()-start));
System.out.println(list.size());
}
// 耗时:2645
线程池插入数据
public static void main(String[] args) throws InterruptedException {
//记录开始时间
Long start = System.currentTimeMillis();
final List<Integer> list = new ArrayList<>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
final Random random = new Random();
//循环add随机数入容器
for (int i=0;i<10000;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
list.add(random.nextInt());
}
});
}
//停止接收新的任务,并且等待未完成任务完成后关闭线程池
executorService.shutdown();
//如果线程超时一天还没关闭,输出线程池没有关闭,直到关闭为止后执行后面代码
while (!executorService.awaitTermination(1, TimeUnit.DAYS)) {
System.out.println("线程池没有关闭");
}
System.out.println("线程池已经关闭");
System.out.println("耗时:"+(System.currentTimeMillis()-start));
System.out.println(list.size());
}
//耗时:58
总结
new Thread:
a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。
Executor:线程池重用存在的线程,减少对象创建、消亡的开销,提升性能
什么是Executor框架?
- Executor的内部实现主要就是线程池来实现,既Executor是通过线程池的方式来控制上层的调度的。所以Executor一定角度上扮演者线程工厂的角色,我们可以通过Executor框架创建特定功能的线程池。
为什么引入Executor框架?
- 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
- 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪。
- 使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现,所以为了解决这一个问题,JDK1.5开始引入了Executor框架用于管理线程和调度线程,可以参考Executor框架的两级调度模型
Executor框架的两级调度模型
在HotSpot 虚拟机线程模型中,Java线程(java.lang.Thread)被一对一的映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程。但该Java线程终止时,这个本地操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU.
而有了Executor框架之后,我们可以将线程调度分为上下两层:
- 上层是Java层面上的应用线程层
- 下层是本地操作系统层面上的底层线程层
应用程序通过Executor框架来控制上层的调度;而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。
线程池框架体系介绍
tips:
普通线程池 ThreadPoolExecutor
定时线程池 ScheduledThreadPoolExecutor
层级 | 名称 | 方法 | 说明 | 类型 |
---|---|---|---|---|
1 | java.util.concurrent.Executor | execute() | 执行接口 | 接口 |
2 | java.util.concurrent.ExecutorService | submit(java.util.concurrent.Callable) | 提交接口 | 接口 |
3 | java.util.concurrent.AbstractExecutorService | submit(java.util.concurrent.Callable) | 把执行和提交合并区别:有返回值和无返回值 | 抽象类 |
4 | java.util.concurrent.ThreadPoolExecutor | execute() | 调 用 addwork( offer>task 放队列)Run 方 法 调 用runwork 方 法getTask(从队列拿数据) | 实现类 |
5 | ava.util.concurrent.ScheduledExecutorService | schedule(),cheduleAtFixedRate(),scheduleWithFixedDelay() | 定义方法,定义接口 | 接口 |
6 | java.util.concurrent.ScheduledThreadPoolExecutor | delayedExecute() | 具体实现add>task>addWork | 实现类 |
-
第一层结构
-
sun.nio.ch.AsynchronousChannelGroupImpl(Iocp) 异步channel –AIO相关实现
-
java.util.concurrent.CompletableFuture.ThreadPerTaskExecutor (启动一个线程执行)
-
sun.net.httpserver.ServerImpl.DefaultExecutor (more执行器,直接执行)
-
com.sun.jmx.remote.internal.ClientNotifForwarder.LinearExecutor (线性执行器)
java.util.concurrent.ExecutorService (核心执行器服务)
-
-
接口简介
- java.util.concurrent.Executor (执行器,执行方法)
- java.util.concurrent.ExecutorService (执行服务) 包含服务的生命周期
- java.util.concurrent.ScheduledExecutorService (调度相关的服务)
-
核心实现类
- java.util.concurrent.ThreadPoolExecutor (普通的的线程池实现类)
- java.util.concurrent.ScheduledThreadPoolExecutor (调度的核心实现类)
-
辅助类
- java.util.concurrent.Executors
-
完成服务
- java.util.concurrent.CompletionService
- java.util.concurrent.ExecutorCompletionService
线程池使用案例
//线程池使用案例
public static void main(String[] args) {
//初始化一个线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//调用submit方法 底层调用的是execute方法
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println("CachedThreadPool线程池");
}
});
executorService.shutdown();
}
其过程如下图所示
- 开始使用newCachedThreadPool初始化一个线程池,初始化后获取到一个ExecutorService并调用submit/execute()方法,创建一个Work对象,使用addWorker入队,调用runWorker如何通过getTask从Worker队列中获取任务,调用run方法就是我们传入的业务代码启动任务,调用完成后会调用peocessWorkerExit方法判断看需不需要继续addWorker
- execute在执行时其实会经过很多的判断,execute的大致流程如下
创建线程池的七中方式
- newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
- newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
- newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
- newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
- newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
- newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
- ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。
线程池执行原理
- ThreadPoolExecutor参数最全的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
• corePoolSize :池中所保存的线程数,包括空闲线程
• maximumPoolSize:池中允许的最大线程数
• keepAliveTime: 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间
• unit:keepAliveTime 参数的时间单位
• workQueue :执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务
• threadFactory:执行程序创建新线程时使用的工厂
• handler :由于超出线程范围和队列容量而使执行
- 线程池运行思路
- 如果当前池大小 poolSize 小于 corePoolSize ,则创建新线程执行任务
- 如果当前池大小 poolSize 大于 corePoolSize ,且等待队列未满,则进入等待队列
- 如果当前池大小 poolSize 大于 corePoolSize 且小于 maximumPoolSize ,且等待队列已满,则创建新线程
执行任务 - 如果当前池大小 poolSize 大于 corePoolSize 且大于 maximumPoolSize ,且等待队列已满,则调用拒绝策
略来处理该任务 - 线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,
- 如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出
- addWorker的方法实现 java.util.concurrent.ThreadPoolExecutor#addWorker
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;// 两种情况1.如果非运行状态 2.不是这种情况(停止状态并且是null对象并且workQueue不等于null)
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;// 判断是否饱和容量了
if (compareAndIncrementWorkerCount(c)) //增加一个work数量 然后跳出去
break retry;
c = ctl.get(); // Re-read ctl 增加work失败后继续递归
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);//增加一个worker
final Thread t = w.thread;
if (t != null) {//判断是否 为null
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired. 锁定后并重新检查下 是否存在线程工厂的失败或者锁定前的关闭
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w); //增加work
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) { //本次要是新增加work成功就调用start运行
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
- java.util.concurrent.ThreadPoolExecutor#execute
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//判断是否小于核心数量,是直接新增work成功后直接退出
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();// 增加失败后继续获取标记
}
//判断是运行状态并且扔到workQueue里成功后
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//再次check判断运行状态如果是非运行状态就移除出去&reject掉
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0) //否则发现可能运行线程数是0那么增加一个null的worker。
addWorker(null, false);
}
else if (!addWorker(command, false)) //直接增加worker如果不成功直接reject
reject(command);
- java.util.concurrent.ThreadPoolExecutor#addWorker
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;// 两种情况1.如果非运行状态 2.不是这种情况(停止状态并且是null对象并且workQueue不等于null)
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;// 判断是否饱和容量了
if (compareAndIncrementWorkerCount(c)) //增加一个work数量 然后跳出去
break retry;
c = ctl.get(); // Re-read ctl 增加work失败后继续递归
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);//增加一个worker
final Thread t = w.thread;
if (t != null) {//判断是否 为null
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired. 锁定后并重新检查下 是否存在线程工厂的失败或者锁定前的关闭
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w); //增加work
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) { //本次要是新增加work成功就调用start运行
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
拒绝策略
线程池有四种拒绝策略:
AbortPolicy:抛出异常,默认
CallerRunsPolicy:不使用线程池执行
DiscardPolicy:直接丢弃任务
DiscardOldestPolicy:丢弃队列中最旧的任务
对 于 线 程 池 选 择 的 拒 绝 策 略 可 以 通 过 RejectedExecutionHandler handler = new
ThreadPoolExecutor.CallerRunsPolicy();来设置。