进程和线程
什么是进程
- 进程是依赖程序存在的。一个进程就是一次程序的执行过程。程序是动态的、进程是静态的。
- 进程是操作系统进行资源调度与分配的基本单位(线程是任务调度与执行的基本单位)
- 一个进程包含多个线程。
- 多进程即指操作系统能同时运行多个任务(程序)
什么是线程
- 线程是是进程锁划分的更小执行单元
- 与进程不同的是同类线程共享代码与数据空间
- 每个线程拥有独立的运行栈、线程之间的切换开销小
线程与进程区别
- 执行单位:进程是操作系统执行的基本单位、线程是处理器任务调度的基本单位。
- 独立性:进程之间互不影响、一个线程可能会影响另外一个线程
- 资源开销:进程都有独立的地址空间、切换资源消耗大、多个线程共享同一个进程内存空间、切换消耗较小
工厂例子
并行与并发
并行:在同一时间内 多个任务同时执行
并发:在同一时间段内,多个任务交替执行、只是看上去同时执行、但不是真正的同时。
线程生命周期
新建状态
使用 new 关键字产生 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。
就绪状态
当调用start方法时、线程不会立即获得cpu使用权、此时就处于就绪状态、等待jvm调度获得cpu使用权。
运行状态
处于就绪状态的线程获得cpu使用权、开始执行run方法。
阻塞状态
处于运行状态的线程因为各种原因让出cpu使用权。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。
- 等待阻塞:运行中的线程调用wait()方法。此时处于等待阻塞状态
- 同步阻塞:运行中的线程获得对象的同步锁时、该同步锁被占用、此时jvm会将该线程饭放到所锁池中。
- 其他阻塞:调用Thread.sleep()、join()、或者发出I/O请求时、进入阻塞状态。当sleep()状态超时,join()等待终止、I/O请求结束。
注意:在一次线程执行中会经历多次就绪—运行—阻塞状态。直到run方法结束。或线程终止。
线程实现
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
- 线程池方式
继承Thread类
public class DemoA extends Thread {
// 运行到这里处于运行状态
@Override
public void run() {
//获取当前线程名
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println(name + "执行了" + i);
}
//run方法执行结束 处于死亡状态
}
}
public static void a() {
//新建状态
DemoA da1 = new DemoA();
DemoA da2 = new DemoA();
//调用start时处于就绪状态 等待获取jvm调度 获取cpu使用权
da1.start();
da2.start();
}
实现Runnable接口
public class DemoB implements Runnable{
@Override
public void run() {
//获取当前线程名
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println(name + "执行了" + i);
}
//run方法执行结束 处于死亡状态
}
}
public static void b() {
//新建任务非新建状态
DemoB demoB1 = new DemoB();
DemoB demoB2 = new DemoB();
//当new Thread及其子类时才处于新建状态
Thread t1 = new Thread(demoB1);
Thread t2 = new Thread(demoB2);
//就绪状态
t1.start();
t2.start();
}
实现Callable 接口
public class DemoC implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 101; i++) {
System.out.println(Thread.currentThread().getName() + "开始计算" + i +
"的值");
sum += i;
}
return sum;
}
}
@SneakyThrows
public static void c() {
//新建任务对象
DemoC c1 = new DemoC();
DemoC c2 = new DemoC();
//对象任务包装
FutureTask<Integer> f1 = new FutureTask<>(c1);
FutureTask<Integer> f2 = new FutureTask<>(c2);
//新建状态
Thread t1 = new Thread(f1);
Thread t2 = new Thread(f2);
//就绪
t1.start();
t2.start();
int a1 = f1.get();
int a2 = f2.get();
System.out.println(a1);
System.out.println(a2);
}
Runnable与Callable区别
- Runnable的run方法没有返回值、Callable的call()方法有返回值。
- Callable的call()方法的异常允许向上抛出、但是Runnable的run方法的异常不允许向上抛出、必须在内部进行处理。
run()与start()区别
调用start方法:线程启动、run()中代码交替执行、体现了多线程特性。
调用run()方法:仅仅只是一个方法调用、并不会启动线程。只是相当于将run方法的调用放在main线程中。
线程同步
即有一个线程对内存进行操作时、其他线程不可以对该内存进行操作。直到该线程完成操作、其他线程才可以操作
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
“同”字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。
线程同步方式
- 同步方法和同步块—synchronized
- 显示锁—Lock
- ThreadLocal 实现线程同步
同步方法
public synchronized void change() {
System.out.println(Thread.currentThread().getName() + "开始对i进行自增操作,目前i:" + i);
i++;
System.out.println(Thread.currentThread().getName() + "结束对i的自增操作,目前i:" + i);
}
同步代码块
synchronized (obj){}
显示锁
ReentrantLock lock = new ReentrantLock();
//lock锁
public void change3() {
System.out.println("这是没有上锁的代码");
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName() + "开始对i进行自增操作,目前i:" + i);
i++;
System.out.println(Thread.currentThread().getName() + "结束对i的自增操作,目前i:" + i);
//解锁
lock.unlock();
}
两种锁的区别
Lock是显示锁(需要手动开锁和关锁)synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁、synchronized是有代码块锁和方法锁
Lock锁效能高、拓展性强。
ThreadLocal 实现线程同步
如果使用ThreadLocal管理变量,则每个使用该变量的线程都获得改变量的副本。
线程安全
代码在多线程和单线程的情况下 运行结果一致。
- 不可变:像 String、Integer、Long 这些,都是 final 类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。
- 绝对线程安全:不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java 中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java 中也有,比方说 CopyOnWriteArrayList、CopyOnWriteArraySet。
- 相对线程安全:相对线程安全也就是我们通常意义上所说的线程安全,像 Vector 这种,add、remove 方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个 Vector、有个线程同时在 add 这个 Vector,99% 的情况下都会出现 ConcurrentModificationException,也就是 fail-fast 机制。
- 线程非安全:这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类。
CopyOnWriteArrayList介绍
它相当于线程安全的ArrayList。和ArrayList一样,它是个可变数组;但是和ArrayList不同的时,它具有以下特性:
- 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
- 它是线程安全的。
- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
- 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
线程调度和通信
Object 类中的 wait、notify、notifyAll,可以用于线程间的通信,通过这三个方法完成线程在指定锁(监视器)上的等待与唤醒,这三个方法是以锁为中心的通信方法。
除了它们之外,还有用于线程调度、控制的方法,他们分别是 sleep、yield 和 join 方法,他们可以用于线程的协作,他们是围绕着线程的调度而来的。
Sleep方法
-
Sleep方法属于Thread类、Sleep方法不会释放锁、只会阻塞线程、让出cpu
-
在指定毫秒数内 让线程暂停执行
-
如果有锁 不会释放锁、所以别的线程也不能使用、
-
可在同步代码块外部或者内部使用 如果有锁不会释放锁 只会阻塞当前线程、让出cpu 其他阻塞
-
如果有锁此时其他线程访问时 因为锁的原因也会进入到阻塞状态 同步阻塞
wait方法
-
wait方法属于Object类、在执行过程中会释放锁
-
wait方法使用时必须线获取锁 必须放在同步代码块中书写
-
执行时会释放锁 让自身进入阻塞状态。
yield 方法
-
让出 cpu 暂停当前正在执行的线程对象、不会释放锁。
-
和wait sleep不同的是 它不会阻塞当前线程 只会让当前线程进入就绪状态、与其他线程继续争夺cpu使用权。
-
所以它使用是 RUNNIING 状态
join方法
- 等待调用 join 方法的线程结束之后、程序再继续执行。
- 一般用于等待异步线程执行完结果之后才能继续运行的场景
线程死锁
当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程死锁了
即有两个线程、线程A、B。线程A有资源a 线程B有资源 b ab 资源只有一份。AB线程都需要获得对方资源才能结束线程 但是AB线程在执行完线程之前都不会释放资源。所形成的循环等待情况、导致程序不能正常结束。
死锁产生的原因
- 互斥条件:在同一时间内、一份资源只能被一个线程所占有、其他线程要想使用只能等待。
- 请求与保持条件:当线程拥有部分资源、申请其他资源失败时、不会释放掉自己已有的资源。
- 不可剥夺条件:一个线程拥有资源后、除非主动释放、否则其他线程不能获取。
- 环路等待条件:线程获取资源形成环路等待。
线程死锁解决方案
解决线程死锁大体来说就是防止四种情况的产生
- 防止互斥:由于使用锁的目的就是产生互斥、所以一般不会使用。
- 防止请求与保持:一次性获取所有执行该线程所需要的全部资源
- 破坏不可剥夺:当线程获取资源失败时、主动释放自己已有的资源。
- 破坏环路等待:可以尝试正序获取资源、反序释放资源。
线程池
线程池好处
-
降低了频繁创建和销毁线程带来的系统开销
-
提高了效率、创建好一定数量的线程放在池中、当需要使用时直接从池中获取、提高了响应速度。
-
提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间。
-
提高了线程的可管理性,频繁的创建和销毁线程会降低系统效率,影响系统稳定性。使用线程池创建和管理线程,在使用时直接从线程池进行获取,使用完放回线程池,避免了线程的重复创建和销毁。
线程池体系结构
线程池的分类
- newCacheTreadPool 可缓存线程池 如果线程池长度超过处理需要,可以灵活回收空闲线程,没回收的话就新建线程
- newFixedThread 创建一个定长的线程池,可控制最大并发数,超出的线程进行队列等待
- newScheduleThreadPool:支持定时周期性任务执行。
- newSingleExecutor:创建一个单线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newCacheTreadPool 方式
创建一个可缓存线程池、如果线程池长度超过需要、可灵活回收空闲线程、若无可回收、则创建新线程。
- 工作线程数量没有限制(严格来说有限制,数目为Interger.MAX_VALUE) 这样可以灵活的往线程中添加线程。
- 如果长时间没有往线程池中提交任务、如果工作线程指定了空闲时间(默认一分钟)则该工作线程自动终止。终止后 如果提交了新任务,则线程池再重新创建一个新的线程。
- 在使用CacheThreadPool时,一定要注意任务的数量。否则由于大量线程同时运行会导致系统瘫痪。
- 适用场景:任务按照提交次序,一个任务一个任务地逐个执行的场景
ExecutorService cached = Executors.newCachedThreadPool();
Cached.submit(Runnable 的实现对象);
newFixedThreadPool 方式
创建一个定长线程池、可控制最大并发数、每当提交一个新任务就创建一个工作线程。如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。 FixedThreadPool 是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
/*
线程池大小为 5,这个参数指定了可以运行的线程的最大数目,超过这个数目的线程加进去以后,不会运行。
其次,加入线程池的线程属于托管状态,线程的运行不受加入顺序的影响。
*/
ExecutorService fixed= Executors.newFixedThreadPool(5);
fixed.submit(Runnable 的实现对象);
newSingleThreadExecutor 方式
创建一个单线程化的 Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO,优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不 会有多个线程是活动的。
// 不管放入多少个 Runnable 实现对象,执行的都是一个线程
ExecutorService sin= Executors.newSingleThreadExecutor();
sin.submit(Runnable 的实现对象);
newScheduleThreadPool 方式
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
ScheduledExecutorService pool=Executors.newScheduledThreadPool(4);
//参数1: task任务
//参数2: 首次执行任务的延迟时间
//参数3: 周期性执行的时间
//参数4: 时间单位
pool.scheduleAtFixedRate(new TargetTask(), 0, 500, TimeUnit.MILLISECONDS);
submit 方法和 execute 方法的区别?
向线程池提交任务的有两种方式:submit和execute。
execute 方法void execute(Runnable command): Executor接口中的方法
submit方法
<T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task);
两种方法区别
- execute() 方法只能接收 Runnable 类型的参数,而 submit() 方法可以接收 Callable、Runnable 两种类型的参数。
- Callable 类型的任务是可以返回执行结果的,而 Runnable 类型的任务不可以返回执行结果。
- submit() 提交任务后会有返回值,而 execute() 没有。
- submit() 方便 Exception 处理。
线程池参数
corePoolSize线程池核心线程大小
corePoolSize 是线程池中一个最小的线程数量,即使这些线程处于空闲状态 也不会被销毁 除非设置了allowCoreThreadTimeOut。
maximumPoolSize线程池最大线程数量
线程池能够容纳同时执行的最大线程数,此值大于等于1。一个任务被提交到线程池以后,首先会找有没有空闲并且存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会放到工作队列中,直到工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。工作队列满,且线程数等于最大线程数,此时再提交任务则会调用拒绝策略。
keepAliveTime 多余的空闲线程存活时间
当线程空闲时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程中的线程数不大于corepoolSIze。
unit 空闲线程存活时间单位
keepAliveTime 的计量单位。
workQueue 工作队列
任务被提交给线程池时,会先进入工作队列,任务调度时再从工作队列中取出。
常用工作队列有以下几种:
-
ArrayBlockingQueue(数组的有界阻塞队列):在创建时必须设置大小,按FIFO排序(先进先出)。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
-
LinkedBlockingQueue(链表的无界阻塞队列):按 FIFO 排序任务,可以设置容量(有界队列),不设置容量则默认使用 Integer.Max_VALUE 作为容量 (无界队列)。该队列的吞吐量高于 ArrayBlockingQueue。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。有两个快捷创建线程池的工厂方法 Executors.newSingleThreadExecutor、Executors.newFixedThreadPool,使用了这个队列,并且都没有设置容量(无界队列)。
-
SynchronousQueue(一个不缓存任务的阻塞队列):生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。其 吞 吐 量 通 常 高 于LinkedBlockingQueue。 快捷工厂方法 Executors.newCachedThreadPool 所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
-
PriorityBlockingQueue(具有优先级的无界阻塞队列):优先级通过参数Comparator实现。
-
DelayQueue(这是一个无界阻塞延迟队列):底层基于 PriorityBlockingQueue 实现的,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,而队列头部的元素是过期最快的元素。快捷工厂方法 Executors.newScheduledThreadPool 所创建的线程池使用此队列。
Java 中的阻塞队列(BlockingQueue)与普通队列相比,有一个重要的特点:在阻塞队列为空时,会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中取元素时,线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。
threadFactory 线程工厂
创建一个线程工厂用来创建线程,可以用来设定线程名、是否为daemon线程等等。
handler 拒绝策略
- AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种)
- DiscardPolicy:丢弃任务,但是不抛出异常
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) 。也就是当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务从队尾添加进去,等待执行。
- CallerRunsPolicy:谁调用,谁处理。由调用线程(即提交任务给线程池的线程)处理该任务,如果线程池已经被shutdown则直接丢弃。
线程池的任务调度流程
- 如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
- 如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。
- 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空。
- 在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
- 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略
线程池的拒绝策略
拒绝情况:
- 线程池已经被关闭
- 工作队列已满且maximumPoolSize已满
几种常见的拒绝策略:
- AbortPolicy(拒绝策略):新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略。
- DiscardPolicy(抛弃策略):新任务就会直接被丢掉,并且不会有任何异常抛出。
- DiscardOldestPolicy(抛弃最老任务策略):将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列(一般队头元素最老)。
- CallerRunsPolicy(调用者执行策略):新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
线程池的生命周期
- RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
- SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
- STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
- TIDYING:该状态下所有任务都已终止或者处理完成,将会执行 terminated() 钩子方法。
AbortPolicy(拒绝策略):新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略。 - DiscardPolicy(抛弃策略):新任务就会直接被丢掉,并且不会有任何异常抛出。
- DiscardOldestPolicy(抛弃最老任务策略):将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列(一般队头元素最老)。
- CallerRunsPolicy(调用者执行策略):新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
线程池的生命周期
- RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
- SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
- STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
- TIDYING:该状态下所有任务都已终止或者处理完成,将会执行 terminated() 钩子方法。
- TERMINATED:执行完 terminated() 钩子方法之后的状态。