多线程相对于其余 Java 知识点来说,有必定的学习门槛,而且了解起来比较费劲。在平时工做中如若使用不当会出现数据错乱、执行效率低(还不如单线程去运行)或者死锁程序挂掉等等问题,因此掌握了解多线程相当重要。面试
本文从基础概念开始到最后的并发模型由浅入深,讲解下线程方面的知识。缓存
本节我将带你们了解多线程中几大基础概念。多线程
并发与并行并发
并行,表示两个线程同时作事情。app
并发,表示一会作这个事情,一会作另外一个事情,存在着调度。单核 CPU 不可能存在并行(微观上)。框架
临界区ide
临界区用来表示一种公共资源或者说是共享数据,能够被多个线程使用。可是每一次,只能有一个线程使用它,一旦临界区资源被占用,其余线程要想使用这个资源,就必须等待。性能
阻塞与非阻塞学习
阻塞和非阻塞一般用来形容多线程间的相互影响。好比一个线程占用了临界区资源,那么其它全部须要这个资源的线程就必须在这个临界区中进行等待,等待会致使线程挂起。这种状况就是阻塞。spa
此时,若是占用资源的线程一直不肯意释放资源,那么其它全部阻塞在这个临界区上的线程都不能工做。阻塞是指线程在操做系统层面被挂起。阻塞通常性能很差,需大约8万个时钟周期来作调度。
非阻塞则容许多个线程同时进入临界区。
死锁
死锁是进程死锁的简称,是指多个进程循环等待他方占有的资源而无限的僵持下去的局面。
活锁
假设有两个线程一、2,它们都须要资源 A/B,假设1号线程占有了 A 资源,2号线程占有了 B 资源;因为两个线程都须要同时拥有这两个资源才能够工做,为了不死锁,1号线程释放了 A 资源占有锁,2号线程释放了 B 资源占有锁;此时 AB 空闲,两个线程又同时抢锁,再次出现上述状况,此时发生了活锁。
简单类比,电梯遇到人,一个进的一个出的,对面占路,两我的同时往一个方向让路,来回重复,仍是堵着路。
若是线上应用遇到了活锁问题,恭喜你中奖了,这类问题比较难排查。
饥饿
饥饿是指某一个或者多个线程由于种种缘由没法得到所须要的资源,致使一直没法执行。
在线程的生命周期中,它要经历建立、可运行、不可运行几种状态。
建立状态
当用 new 操做符建立一个新的线程对象时,该线程处于建立状态。
处于建立状态的线程只是一个空的线程对象,系统不为它分配资源。
可运行状态
执行线程的 start() 方法将为线程分配必须的系统资源,安排其运行,并调用线程体——run()方法,这样就使得该线程处于可运行状态(Runnable)。
这一状态并非运行中状态(Running),由于线程也许实际上并未真正运行。
不可运行状态
当发生下列事件时,处于运行状态的线程会转入到不可运行状态:
调用了 sleep() 方法;
线程调用 wait() 方法等待特定条件的知足;
线程输入/输出阻塞;
返回可运行状态;
处于睡眠状态的线程在指定的时间过去后;
若是线程在等待某一条件,另外一个对象必须经过 notify() 或 notifyAll() 方法通知等待线程条件的改变;
若是线程是由于输入输出阻塞,等待输入输出完成。
线程优先级及设置
线程的优先级是为了在多线程环境中便于系统对线程的调度,优先级高的线程将优先执行。一个线程的优先级设置听从如下原则:
线程建立时,子继承父的优先级;
线程建立后,可经过调用 setPriority() 方法改变优先级;
线程的优先级是1-10之间的正整数。
线程的调度策略
线程调度器选择优先级最高的线程运行。可是,若是发生如下状况,就会终止线程的运行:
线程体中调用了 yield() 方法,让出了对 CPU 的占用权;
线程体中调用了 sleep() 方法,使线程进入睡眠状态;
线程因为 I/O 操做而受阻塞;
另外一个更高优先级的线程出现;
在支持时间片的系统中,该线程的时间片用完。
单线程建立方式
单线程建立方式比较简单,通常只有两种方式:继承 Thread 类和实现 Runnable 接口;这两种方式比较经常使用就不在 Demo 了,可是对于新手须要注意的问题有:
不论是继承 Thread 类仍是实现 Runable 接口,业务逻辑是写在 run 方法里面,线程启动的时候是执行 start() 方法;
开启新的线程,不影响主线程的代码执行顺序也不会阻塞主线程的执行;
新的线程和主线程的代码执行顺序是不可以保证前后的;
对于多线程程序,从微观上来说某一时刻只有一个线程在工做,多线程目的是让 CPU 忙起来;
经过查看 Thread 的源码能够看到,Thread 类是实现了 Runnable 接口的,因此这两种本质上来说是一个;
PS:平时在工做中也能够借鉴这种代码结构,对上层调用来说提供更多的选择,做为服务提供方核心业务归一维护
经过上面的介绍,彻底能够开发一个多线程的程序,为何还要引入线程池呢。主要是由于上述单线程方式存在如下几个问题:
线程的工做周期:线程建立所需时间为 T1,线程执行任务所需时间为 T2,线程销毁所需时间为 T3,每每是 T1+T3 大于 T2,全部若是频繁建立线程会损耗过多额外的时间;
若是有任务来了,再去建立线程的话效率比较低,若是从一个池子中能够直接获取可用的线程,那效率会有所提升。因此线程池省去了任务过来,要先建立线程再去执行的过程,节省了时间,提高了效率;
线程池能够管理和控制线程,由于线程是稀缺资源,若是无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行统一的分配,调优和监控;
线程池提供队列,存放缓冲等待执行的任务。
大体总结了上述的几个缘由,因此能够得出一个结论就是在平时工做中,若是要开发多线程程序,尽可能要使用线程池的方式来建立和管理线程。
经过线程池建立线程从调用 API 角度来讲分为两种,一种是原生的线程池,另外该一种是经过 Java 提供的并发包来建立,后者比较简单,后者实际上是对原生的线程池建立方式作了一次简化包装,让调用者使用起来更方便,但道理都是同样的。因此搞明白原生线程池的原理是很是重要的。
经过 ThreadPoolExecutor 建立线程池,API 以下所示:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue);
先来解释下其中的参数含义(若是看的比较模糊能够大体有个印象,后面的图是关键)。
corePoolSize
核心池的大小。
在建立了线程池后,默认状况下,线程池中并无任何线程,而是等待有任务到来才建立线程去执行任务,除非调用了 prestartAllCoreThreads() 或者 prestartCoreThread() 方法,从这两个方法的名字就能够看出,是预建立线程的意思,即在没有任务到来以前就建立 corePoolSize 个线程或者一个线程。默认状况下,在建立了线程池后,线程池中的线程数为0,当有任务来以后,就会建立一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。
maximumPoolSize
线程池最大线程数,这个参数也是一个很是重要的参数,它表示在线程池中最多能建立多少个线程。
keepAliveTime
表示线程没有任务执行时最多保持多久时间会终止。默认状况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起做用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,若是一个线程空闲的时间达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。
可是若是调用了 allowCoreThreadTimeOut(boolean) 方法,在线程池中的线程数不大于 corePoolSize 时,keepAliveTime 参数也会起做用,直到线程池中的线程数为0。
unit
参数 keepAliveTime 的时间单位。
workQueue
一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,通常来讲,这里的阻塞队列有如下这几种选择:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。
threadFactory
线程工厂,主要用来建立线程。
handler
表示当拒绝处理任务时的策略,有如下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常;
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,可是不抛出异常;
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,而后从新尝试执行任务(重复此过程);
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
上面这些参数是如何配合工做的呢?请看下图:
注意图上面的序号。
简单总结下线程池之间的参数协做分为如下几步:
线程优先向 CorePool 中提交;
在 Corepool 满了以后,线程被提交到任务队列,等待线程池空闲;
在任务队列满了以后 corePool 尚未空闲,那么任务将被提交到 maxPool 中,若是 MaxPool 满了以后执行 task 拒绝策略。
流程图以下:
以上就是原生线程池建立的核心原理。除了原生线程池以外并发包还提供了简单的建立方式,上面也说了它们是对原生线程池的一种包装,可让开发者简单快捷的建立所须要的线程池。
newSingleThreadExecutor
建立一个线程的线程池,在这个线程池中始终只有一个线程存在。若是线程池中的线程由于异常问题退出,那么会有一个新的线程来替代它。此线程池保证全部任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool
建立固定大小的线程池。每次提交一个任务就建立一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,若是某个线程由于执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool
可根据实际状况,调整线程数量的线程池,线程池中的线程数量不肯定,若是有空闲线程会优先选择空闲线程,若是没有空闲线程而且此时有任务提交会建立新的线程。在正常开发中并不推荐这个线程池,由于在极端状况下,会由于 newCachedThreadPool 建立过多线程而耗尽 CPU 和内存资源。
newScheduledThreadPool
此线程池能够指定固定数量的线程来周期性的去执行。好比经过 scheduleAtFixedRate 或者 scheduleWithFixedDelay 来指定周期时间。
PS:另外在写定时任务时(若是不用 Quartz 框架),最好采用这种线程池来作,由于它能够保证里面始终是存在活的线程的。
在阿里的 Java 开发手册时有一条是不推荐使用 Executors 去建立,而是推荐去使用 ThreadPoolExecutor 来建立线程池。
这样作的目的主要缘由是:使用 Executors 建立线程池不会传入核心参数,而是采用的默认值,这样的话咱们每每会忽略掉里面参数的含义,若是业务场景要求比较苛刻的话,存在资源耗尽的风险;另外采用 ThreadPoolExecutor 的方式可让咱们更加清楚地了解线程池的运行规则,不论是面试仍是对技术成长都有莫大的好处。
改了变量,其余线程能够当即知道。保证可见性的方法有如下几种:
volatile
加入 volatile 关键字的变量在进行汇编时会多出一个 lock 前缀指令,这个前缀指令至关于一个内存屏障,内存屏障能够保证内存操做的顺序。当声明为 volatile 的变量进行写操做时,那么这个变量须要将数据写到主内存中。
因为处理器会实现缓存一致性协议,因此写到主内存后会致使其余处理器的缓存无效,也就是线程工做内存无效,须要从主内存中从新刷新数据。