1.多线程
1)什么是多线程
多线程就是在同一时刻干多件事情
实现在同一时刻干多件事情的方式有两种(CPU) :
-
并行 : 多个核心在同一时刻"同时"干多件事情
-
并发 : 单个核心在同一时刻"快速交替"干多件事情
快速交替 : 其实就是cpu执行过程中并不是一个程序执行完之后cpu才切换, 而是cpu时间片用完, 就会切换到下个线程执行, 给人一种多个程序同时执行的感觉
2)多线程的好处
多线程可以在"单位时间内"处理"复杂且庞大"的数据或业务时提升效率
-
在多核cpu中, 利用多线程可以实现并行执行
-
同步处理的流程容易发送阻塞, 可以用线程来实现异步处理, 提高程序处理实时性
-
线程可以认为是轻量级的进程, 所以线程的创建、销毁比进程更快(性能开销更小)
(举例说明A服务需要B C D的数据 如果是单线程的话只能一个一个的拿, 使用多线程可以同时拿到所有需要的数据)
3)多线程的弊端
-
如果并发的线程数量很多, 并且每个线程都是执行一个世纪很短的任务就结束了, 这样频繁创建线程就会大大降低系统的效率, 因为频繁创建线程和销毁线程需要时间
-
如果大量线程在执行, 会涉及到线程间上下文切换, 会极大的消耗cpu运算资源
4)如何创建线程?
-
继承Thread
-
实现Runnable接口
-
使用Callable接口(可以使用CompletableFuture)
注意 :
我们项目中使用多线程编程一定要使用线程池, 否则可能会导致线程池创建过多发生异常
5)线程安全
多个线程在对共享数据进行读改写的时候, 可能导致的数据错乱就是线程的安全问题了
如何判断当前程序中是否存在线程安全问题?
-
是否存在多线程环境
-
在多线程环境下是否存在共享变量
-
在多线程环境下是否存在对共享变量"写"操作
2. 线程的生命周期? 线程有几种状态
-
新建状态(New):新创建了一个线程对象。
-
就绪状态(Runnable):线程对象创 建后,其他线程调用了该对象的start方法。该状态的线程位于 可运行线程池中,变得可运行,等待获取CPU的使用权。
-
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
-
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
-
死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
阻塞的情况又分为三种 :
-
等待阻塞 : 运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待 池”中。进入这个状态后,是不能自动唤醒的,必依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法
-
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
-
其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 sleep是Thread类的方法
3. wait和sleep的区别
共同点 :
-
wait(), wait(long)和sleep(long)的效果都是让当前线程暂时放弃cpu的使用权, 进入阻塞状态
不同点 :
-
方法归属不同
-
sleep(long)是Thread的静态方法
-
wait(), wait(long)都是Object的成员方法, 每个对象都有
-
-
醒来时机不同
-
执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
-
wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
-
它们都可以被打断唤醒
-
-
锁特性不同(重点)
-
-
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
-
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
-
而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
-
4.volatile的作用和原理
1) JVM内存模型
JVM让java程序与硬件指令进行了隔离
由于JVM运行程序的实体是线程,创建每个线程时,java 内存模型会为其创建一个工作内存(我们一般称为栈),工作内存是每个线程的私有数据区域。
Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。
但线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。
2) java并发编程要解决的三个问题(三大特征)
原子性
一个线程在CPU中操作不可暂定,也不可中断,要不执行完成,要不不执行
内存可见性
默认情况下变量,当一个线程修改内存中某个变量时,主内存值发生了变化,并不会主动通知其他线程,即其他线程并不可见
有序性
程序执行的顺序按照代码的先后顺序执行。
3) Volatile
volatile关键字, 保证内存可见性, 永远都从内存中去读, 不要使用JIT优化成true 它并不能保证线程安全
5. 为什么用线程池? 解释下线程池参数
-
降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
-
提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
-
提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
/* corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会 消除,而是一种常驻线程 maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程 数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但 是线程池内线程总数不会超过最大线程数 keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会 消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间, 也就是核心线程不会 setKeepAliveTime 来设置空闲时间 workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放 入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程 ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建 工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择 自定义线程工厂,一般我们会根据业务来制定不同的线程工厂 Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这 时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程 池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提 交的任务时,这是也就拒绝 */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
-
java中常见的几种线程池
// 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程, //若无可回收,则新建线程。 Executors.newCachedThreadPool();// //创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 Executors.newFixedThreadPool(10); //创建一个定长线程池,支持定时及周期性任务执行。 Executors.newScheduledThreadPool(10);// 核心线程数10 //创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务, //保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 Executors.newSingleThreadExecutor();
-
线程池执行顺序
6. 项目中线程池的使用
-
tomcat自带线程池
-
CompletableFuture 创建线程时指定线程池,防止创建线程过多
7. synchronized锁释放时机
● 当前线程的同步方法、代码块执行结束的时候释放
1) 正常结束
2) 异常结束出现未处理的error或者exception导致异常结束的时候释放
● 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁
8.Sychronized和ReentrantLock的区别
-
sychronized是⼀个关键字,ReentrantLock是⼀个类
-
sychronized的底层是JVM层⾯的锁(底层由C++ 编写实现),ReentrantLock是API层⾯的锁 (java 内部的一个类对象)
-
sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
-
sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
注: 假设多个线程都要获取锁对象,满足先等待的线程先获得锁则是公平锁,否则是非公平锁
-
sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
-
sychronized底层有⼀个锁升级的过程(访问对象线程数由少到多,竞争由不激烈到激烈,底层会通过一种锁升级机制 无锁->偏向锁->轻量级锁->重量级锁,保证性能) ,会使用自旋 线程频繁等待唤醒会浪费性能,特别是锁的获取也许只需要很短的时间 ,不限于等待,直接执行简单代码while(true)执行完抢锁 来优化性能
9. 悲观锁 VS 乐观锁
-
悲观锁的代表是 synchronized 和 Lock 锁
-
-
其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
-
线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
-
实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
-
-
乐观锁的代表是 AtomicInteger AtomicStampReference,使用 cas 来保证原子性
-
-
其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
-
由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
-
它需要多核 cpu 支持,且线程数不应超过 cpu 核数
-
10. ConcurrentHashMap的原理
1) JDK1.7时
-
数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
-
并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了(默认16,可以指定)
-
扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
-
其他Segment首次创建小数组时,会以Segment[0] 为原型为依据,数组长度,扩容因子都会以原型为准
2) JDK1.8时
-
数据结构:Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突
-
并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容
-
扩容条件:Node 数组满 3/4 时就会扩容(0.75 扩容因子)
-
扩容时并发 get
-
-
根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
-
-
扩容时并发 put
-
-
如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
-
11. Hashtable 和 ConcurrentHashMap 有什么区别?其底层实现是什么?
-
Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
-
Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
-
ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
12. ThreadLocal
-
线程本地对象, 数据保存在当前咸成忠, 属于线程安全
-
应用场景 : 接受请求后, 请求头中包含用户信息, 可以在拦截器中将数据存放到ThreaLocal中, 这样后续service和controller都可以获取到这个数据, 保证线程安全
-
原理 : 底层数据结构是一个ThreadLocalMap, 每个线程都持有一个, key是ThreadLocal对象, value是他的值