1.什么是线程
1.1
在这之前,首先让我们来了解下在操作系统中进程和线程的区别:
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,多个线程之间共享进程的代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
1.2
分为用户线程和守护线程
一般来说,线程默认为用户线程
守护线程是什么
当所有的用户线程全都执行完毕,守护线程直接结束
垃圾回收机制典型守护线程
void setDaemon(boolean on) 将此线程标记为 daemon线程或用户线程。
2 线程的创建方式
四种方式
1 继承Thread,重写run方法 + start开启线程
2 实现Runnable接口,重写run方法 + start开启线程
优点:
接口多实现,类的单继承
实现资源共享
3 实现Callable接口,重写call方法 + 线程池
开启线程第三种方式 : (了解)
实现juc包下的Callable接口,重写call方法 + 线程池
优点:
call方法可以抛出异常,可以定义返回值,run方法不可以
4 创建线程池
(之后讲)
3 线程的状态以及调度的方式
3.1五种状态
首先线程有五种状态
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程变得可运行,等待获取CPU的使用权。
- 如何让线程进入到就绪状态:
1.start()
2.阻塞解除
3.cpu的调度切换
4.yield 礼让线程
(yield 礼让线程)当线程执行到yield方法,会让出cpu的资源,同时线程会恢复就绪状态
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。cpu调度
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态。
- 如何让线程进入阻塞状态:
1.sleep()
2.join() 插队线程
3.wait() 等待
4.IO
(sleep 线程休眠)当一个线程调度sleep进入睡眠状态,让出cpu的资源
抱着资源睡觉: 这个资源不是cpu的资源, 值的是对象的锁资源
(join() 插队线程) A线程执行过程中,如果B线程插队了,A线程就会进入到阻塞状态,等待插队线程执行完毕|,A线程会恢复到就绪状态
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
- 如何让线程进入终止状态:
1.正常执行完毕
2.stop() 过时–> 不推荐使用
3.通过标识判断
① void interrupt() 为线程添加一个中断标识
②boolean isInterrupted() 测试此线程是否已被中断,是否曾经调用过interrupt方法添加了中断标识
③static boolean interrupted() 测试当前线程是否已被中断,是否曾经调用过interrupt方法添加了中断标识,同时复位标识
4.线程的安全问题
4.1 前提条件
(1)有多个线程
(2)共享数据
(3)多条语句操作共享数据
4.2 同步锁
4.2.1 乐观锁
CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的
CAS 是怎么实现线程安全的?
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
ABA 问题:
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 “ABA”问题。
ABA 问题解决:
我们需要加上一个版本号(Version),在每次提交的时候将版本号+1操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息~
4.2.2 悲观锁
synchronized关键字最主要有以下3种应用方式,下面分别介绍
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
1、乐观锁并未真正加锁,所以效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
2、悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
4.2.3 如何选择
悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。
但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
5.线程池
总的来说
线程池有如下的优势:
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
5.1 七个参数
而我们创建时,一般使用它的子类:ThreadPoolExecutor,有七个参数如下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
1.corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
2.maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
3.keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
4.unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
5.workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
6.threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
7.handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
5.2 线程池创建、接收任务、执行任务、回收线程的步骤
创建线程池后,线程池的状态是Running,该状态下才能有下面的步骤
1.提交任务时,线程池会创建线程去处理任务
2.当线程池的工作线程数达到corePoolSize时,继续提交任务会进入阻塞队列
3.当阻塞队列装满时,继续提交任务,会创建救急线程来处理
4.当线程池中的工作线程数达到maximumPoolSize时,会执行拒绝策略
5.当线程取任务的时间达到keepAliveTime还没有取到任务,工作线程数大于corePoolSize时,会回收该线程
注意: 不是刚创建的线程是核心线程,后面创建的线程是非核心线程,线程是没有核心非核心的概念的,这是我长期以来的误解。
5.3 拒绝策略
1.AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
2.CallerRunsPolicy:由调用线程处理该任务。
3.DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
4.DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。