进程与线程:
进程:可以简单理解为运行中的程序。
进程特点:【待完善】
线程:进程中的顺序执行流。
进程特点:【待完善】
线程六大状态:
NEW、RUNNABLE、BLOCK、WAITING、TIMED_WAITING、TERMINATED
- 当使用extend Thread、implement Runnable、implement Callable
- 当执行run()方法时,线程状态从NEW变为RUNNABLE
- 如果执行了wait() join()等方法主动暂停了线程,线程进入WAITING状态,进入该状态不释放占用的锁。
- 如果执行了带超时时间的waitsleepjoin方法主动暂停了线程,线程进入TIMED_WAITING状态,进入该状态同样不释放占用的锁。
- 如果线程运行到synchronized修饰的代码段或者使用Lock类进行加锁的代码时,如果无法获取到锁,即进入BLOCK状态
- 如果线程在BLOCK状态下获取到锁、如果线程在WAITING状态下被主动唤醒、如果线程在TIMED_WAITING状态下被主动唤醒或者超时时间到,即进入RUNNABLE状态。
- 当线程运行完毕或者遇到意外退出时,状态变为TERMINATED
- 学习操作系统时,会有READY和RUNNING这两个概念,其实应该是针对进程的说法,因为线程内部每次运行时间都特别短,READY与RUNNING切换特别频繁,没有必要严格继承操作系统那一套状态转移,所以Thread类源码中对此的定义也说明了:A thread can be in only one state at a given point in time.These states are virtual machine states which do not reflect any operating system thread states.意思是线程状态其实是虚拟机定义的状态,与操作系统thread的状态无关。(参考自 https://www.zhihu.com/question/56494969)
-
- *
Java多线程回调:
这一块我现在也没弄明白,之后再补充吧。
启动线程的方法:
- 继承
Thread
类 最简单的方法
public class Task extends Thread{
private String tName;
@Override
public void run(){
try{
System.out.println("线程" + tName + Thread.currentThread() + "正在运行!");
Thread.sleep(3000);
}
catch (Exception e){
e.getMessage();
e.printStackTrace();
}
}
public Task(String tName){
this.tName = tName;
}
public static void main(String[] args) {
Thread task1 = new Task("任务1");
Thread task2 = new Task("任务2");
Thread task3 = new Task("任务3");
task1.start();
task2.start();
task3.start();
}
}
- 实现
Runnable
接口 需要作为Thread的target传入,仍然是以Thread类运行
public class Task2 implements Runnable{
private String tName;
@Override
public void run(){
try{
System.out.println("线程" + tName + Thread.currentThread() + "正在运行!");
Thread.sleep(3000);
}
catch (Exception e){
e.getMessage();
e.printStackTrace();
}
}
public Task2(String tName){
this.tName = tName;
}
public static void main(String[] args) {
Thread task1 = new Thread(new Task2("任务1"));
Thread task2 = new Thread(new Task2("任务2"));
Thread task3 = new Thread(new Task2("任务3"));
task1.start();
task2.start();
task3.start();
}
}
- 实现
Callable
接口,配合FutureTask使用 具有返回值,可使用返回值进行交互等操作。
public class Task3 implements Callable {
private String tName;
@Override
public Integer call(){
try{
System.out.println("线程" + tName + Thread.currentThread() + "正在运行!");
Thread.sleep(3000);
}
catch (Exception e){
e.getMessage();
e.printStackTrace();
}
return 0;
}
public Task3(String tName){
this.tName = tName;
}
public static void main(String[] args) {
try{
FutureTask futureTask1 = new FutureTask<>(new Task3("任务1"));
//第一种运行方法
futureTask1.run();
//第二种运行方法
new Thread(futureTask1).start();
System.out.println("线程执行结束返回值为:" + futureTask1.get());
}catch (Exception e){
e.printStackTrace();
}
}
}
三种启动线程方法的对比:
由于Java具有单继承,多实现的特性,一般常用实现Runnable
或实现Callable
的方式使用多线程,毕竟每个类只有一个继承的机会,如果给了Thread,灵活性就大打折扣。如果不关心线程的运行结果,使用继承Runnable
即可,如果需要监控线程运行状态以便进行后续处理的话,只能使用实现Callable
方法,传入对象作为进程间通信的公共访问区,可以灵活使用。
多线程并发就得防止同时读写造成的冲突,也就需要锁机制:
一般来说锁有两种类型:
独占锁:占锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和 JUC(java.util.concurrent 并发工具包的缩写)中Lock的实现类就是独占锁。
共享锁:共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。 独享锁与共享锁也是通过AQS(AbstractQueuedSynchronizer 后面会有详解)来实现的,通过实现不同的方法,来实现独享或者共享。
Java中常用的的独占锁有两种实现,synchronized关键字与Lock接口。
synchronized:
这是一个Java内置关键字,也可以java内置的一个特性。当一个线程访问一个被synchronized修饰的代码块,会自动获取对应的一个锁,并在执行该代码块时,其他线程想访问这个代码块,会一直处于等待状态,只有等该线程释放锁后,其他线程进行资源竞争,竞争获取到锁的线程才能访问该代码块。
线程释放synchronized修饰的代码块锁的方式有两种:
- 该线程执行完对应代码块,自动释放锁。(正常释放)
- 在执行该代码块是发生了异常,JVM会自动释放锁。(异常释放)
Lock:
Lock是一个接口,方法定义如下:
void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
void lockInterruptibly() // 和 lock()方法相似, 但阻塞的线程可中断,抛出 java.lang.InterruptedException异常
boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回true
boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
void unlock() // 释放锁
实现Lock接口的类有很多,以下为几个常见的锁实现
- ReentrantLock:表示重入锁,它是唯一一个直接实现了Lock接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数
- ReentrantReadWriteLock:重入读写锁,它实现了ReadWriteLock接口,在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
- StampedLock: stampedLock是JDK8引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。相比于普通的ReentranReadWriteLock主要多了一种乐观读的功能。(该锁的详细说明可以看这篇帖子https://www.cnblogs.com/myworld7/p/12332911.html,之后我可能会整合到这篇文章中)
下面是两种实现互斥锁的方法的对比表格:
维度 | synchronized | Lock |
---|---|---|
存在层次 | 关键字,在JVM层 | 是一个类,在应用层 |
获取锁 | 自动获取,获取不到则一直等待 | 手动获取,可灵活处理获取不到的情况 |
释放锁 | (1. 执行完释放 2.抛出异常释放)有且仅有这两种自动释放 | 手动释放,支持超时释放 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入,不可中断,非公平 | 可重入,可中断,可公平 |
场景 | 适合少量代码实现同步 | 适合大量代码实现同步 |
什么是AQS
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包内。是实现Lock家族的核心类,基础类。
AQS的核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
——CLH(Craig,Landin,and Hagersten)队列,一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
(CLH详解 可以参考该文章 https://blog.csdn.net/firebolt100/article/details/82662102
)
AQS详解可以看这个文章:https://blog.csdn.net/GV7lZB0y87u7C/article/details/92260574
说了这么多线程的事情,可是实际开发场景中,大家都喜欢用线程池,那么下面来说说线程池相关的事情:
什么是池?
“池”是一种思想策略,池化策略的核心本质是复用。
许多开发场景中,比如使用线程和数据库连接时,创建、销毁一个线程或数据库连接所需要的开销较大,这种情况下,池化策略就应运而生。这种策略可以最大限度地避免进行创建和销毁,将已经创建的线程或数据库连接保留在“池”这个容器中,随用随取,用完放回,支持复用。这样就省去了创建的代价,但是多出了维护“池”的代价,也就是复用的代价。
当经过评估,发现“复用的代价”远远小于“创建的代价”,则应该考虑使用池化策略。有些场景下 “创建的代价”远远小于“复用的代价”,此时就不必使用池化策略了。
线程池的创建:
支持手动和自动创建。
手动创建:new ThreadPoolExecutor 自定义各项参数
自动创建:Executor.function() 通过简单参数创建指定类型线程池
线程池核心类:ThreadPoolExecutor
我们先看该类的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//略
}
ThreadPoolExecutor类中一共提供了四个构造方法,其中前三个内部都是调用了最后一个构造方法,所以我们可以理解为,给出了三种常用参数搭配的构造方法以及一个根本的构造方法。
ThreadPoolExecutor构造函数重要入参解析:
在这里给出定义,后面看到线程池工作流程图时就会更加清晰。
int corePoolSize
:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务int maximumPoolSize
:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)long keepAliveTime
:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程,所以说这个参数只针对非核心线程管用);TimeUnit unit
:keepAliveTime的时间单位BlockingQueue
:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中(后面会详细解释每种队列)workQueue ThreadFactory threadFactory
:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建RejectedExecutionHandler handler
:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
(后面会详细解释每种策略)
线程池创建线程流程图:
对这张图,我刚开始还真有个地方没想明白,那就是提交任务后的第一个判断。假设线程池是刚创建好的,核心线程数3,任务队列长度5,最大线程数10(额外线程数为10-3=7),初始化状态下核心线程池为空。此时提交任务,一定会在核心线程池中创建新的线程,然后把任务交给该线程去执行。假设第一个任务执行完毕,此时核心线程池里有一个空闲进程,此时又来一个新任务,该任务不会交给核心线程池里的那一个空闲进程,而是新创建一个线程,交给它去做。重复该逻辑直到核心线程池中线程数达到3。当核心线程池中线程数达到上限时,此时来一个新任务,线程池会把这个任务放到任务队列中去,而不是直接交给核心线程池的一个空闲线程来做。
workQueue队列
有下面几种常见的队列:
(1)ArrayBlockingQueue
:规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
(2)LinkedBlockingQueue
:大小不固定的BlockingQueue,若其构造时可以指定队列长度,最大值为Integer.MAX_VALUE,如果不指定长度,默认为最大值。当不指定队列长度时,可视为无界队列。所含的对象是FIFO顺序排序的。
(3)PriorityBlockingQueue
:类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定,也就是支持自定义优先级比较策略。默认队列大小是11。
(4)SynchronizedQueue
:与无界情况下的LinkedBlockingQueue正好相反,没有任务队列,接收到任务直接转发给空闲线程或执行拒绝策略,也就是上文说到的同步提交。
(5)DelayedWorkQueue
:ScheduledThreadPoolExecutor 内部的一个基于时间的任务队列,按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,方便任务的执行。
Executors工具类中为我们提供了几种线程池:
自动创建线程池的几种方式都封装在Executors工具类中:
newFixedThreadPool
:使用的构造方式为
new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())
,设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常
newSingleThreadExector
:使用的构造方式为
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0)
,基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致
newCachedThreadPool
:使用的构造方式为
new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue())
,corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM
newScheduledThreadPool
:使用的构造方式为
new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue())
,支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致
所以根据上面分析我们可以看到,FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了corePoolSize=maxPoolSize,线程数无法动态扩展,于是就设置了无界阻塞队列来应对不可知的任务量;而CachedThreadPool则使用的是SynchronousQueue同步移交队列,因为CachedThreadPool设置了corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,用无限的备用线程数来执行任务,就用不到队列来存储任务;
SchduledThreadPool用的是延迟队列DelayedWorkQueue 一般来说这个线程池就与这个队列关联。
在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规范》中是这样描述的:
从这些默认的线程池的参数我们也能看出来,这种默认给最大长度队列以及最大限度线程上限的做法很极端,容易出现OOM,所以开发中最好自己手动创建,根据需求制定最合适且性能最好的线程池是最合适的做法。
所以后续就会有线程池参数调优这个操作,找到适合当前业务场景最合适的线程池属性。
handler拒绝策略
- AbortPolicy:中断抛出异常(问题报告给老板,让老板处理)
- DiscardPolicy:默默丢弃任务,不进行任何通知(悄悄丢弃问题,不告诉老板)
- DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务(把最早出现的问题扔掉,该问题放入待解决列表)
- CallerRunsPolicy:让提交任务的线程去执行任务(让老板去解决问题)
用完了线程池,如何关闭?
- shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表(长按电源键-关机)
- shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略(开始-关机界面-确定关机-关机动画-关机)
简单描述是这样,但是还有很多的细节,比如调用shutdown方法后,正在执行任务的线程做出什么反应?正在等待任务的线程又做出什么反应?线程在什么情况下才会彻底退出。如果不了解这些细节,在关闭线程池时就难免遇到,像线程池关闭不了,关闭线程池出现报错等情况。
再说这些关闭线程池细节之前,需要强调一点的是,调用完shutdownNow和shuwdown方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination方法进行同步等待。
如何合理的配置Java线程池
线程池究竟设置多大要看你的线程池执行的什么任务了,CPU密集型、IO密集型、混合型,任务类型不同,设置的方式也不一样
任务一般分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池
CPU密集型(频繁计算)
尽量使用较小的线程池,一般CPU核心数+1
因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销
问:上文解释只说明了为啥这种情况下线程数不能太大,但是为啥一般情况下设定为CPU核心数+1?
答:- 通俗解释:对于计算密集型的程序,线程数应当等于核心数,但是再怎么计算密集,总有一些IO等能够将CPU让出来的操作吧,所以再加一个线程来把等待IO的CPU时间利用起来
- 严谨解释:对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的效率。(即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。)
- IO密集型(频繁读写)
方法一:可以使用较大的线程池,一般CPU核心数 * 2
IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间
方法二:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
下面举个例子:
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
3、混合型
可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定