线程
线程的基础知识
线程与进程的区别
当一个程序被运行,从磁盘加载这个程序的代码至内存,就开启了一个
进程
一个进程之内可以分成一到多个线程,一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
并行和并发的区别
在多核CPU下,
并发是同一时间应对多件事情的能力,多个线程轮流使用同一个或多个CPU
并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
创建线程的方式有哪些
- 通过继承Thread类并重写run方法
- 实现Runnable接口并重写run方法
- 实现Callable接口并重写call方法
- 通过线程池创建线程
继承Thread类
实现Runnable接口
实现Callable接口
通过线程池创建
Runnable和Callable接口创建线程有什么区别呢?
- Runnable接口run方法没有返回值
- Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法不能抛出异常,只能捕获异常
run()方法和start()方法有什么区别
- start(): 用来启动线程,通过该线程调用run方法执行run方法所定义的逻辑代码。start方法只能被调用一次
- run() : 封装了要被线程执行的代码,可以被调用多次。
线程包含了哪些状态,状态之间是如何切换的
- 创建线程对象是
新建状态
- 调用了start()方法转变为
可执行状态
- 线程获取到了CPU的执行权,执行结束是
终止状态
- 在可执行状态的过程中,如果没有获取CPU的执行权,有可能会切换成其他状态
- 如果没有获取锁(synchronized或lock)进入
阻塞状态
,获得锁后再切换为可执行状态 - 如果线程调用了wait()方法进入
等待状态
,其他线程调用notifiy()唤醒后可切换为可执行状态 - 如果线程调用了sleep(时间)方法,进入
计时等待状态
,到时间后自动理念切换为可运行状态
- 如果没有获取锁(synchronized或lock)进入
在Java中sleep和wait方法的不同
- 相同点: wait()、sleep(long)、sleep()都是让当前线程暂时放弃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,你们也用不了)
-
新建三个线程,如何保证它们按顺序执行
可以使用join()方法解决
join()等待线程运行结束
notify 和 notifyAll有什么区别
- notify :随机唤醒一个wait的线程
- notifyAll:唤醒所有wait的线程
如何停止一个正在运行的线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
- 打断阻塞的线程( sleep,wait,join )的线程,线程会抛出InterruptedException异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
线程中并发安全
synchronized关键字的底层原理
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住
- Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
- 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner、entrylist、waitset
- 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
synchronized关键字的底层原理 进阶
重量级锁
Monitor实现的锁属于
重量级锁
,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
在JDK 1.6引入了两种新型锁机制:偏向锁
和轻量级锁
,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
对象是怎么关联上的Monitor
对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充
轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
加锁流程
- 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
- 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次
锁重入
。设置Lock Record第一部分为null,起到了一个重入计数器的作用。 - 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁流程
- 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
- 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
- 如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
锁的应用场景
你谈谈JMM(Java内存模型)
JMM(Java Memory Model)Java内存模型,定义了
共享内存
中多线程程序读写操作
的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
CAS知道嘛
CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,
在无锁情况下保证线程操作共享数据的原子性
。
在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作
AbstractQueuedSynchronizer(AQS框架)
AtomicXXX类
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
乐观锁和悲观锁的区别
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
什么是AQS
全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的
基础框架
- AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
- 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
ReentrantLock的实现原理
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
总结
- ReentrantLock表示支持重新进入的锁,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞
- ReentrantLock主要利用CAS+AQS队列来实现
- 支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁
synchronized和Lock有什么区别
-
语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
-
功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)
-
性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
死锁产生的条件是什么
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
此时程序并没有结束,这种现象就是死锁现象…线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁。
如何进行死锁诊断
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
还可以使用其他可视化工具
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息
可视化工具
- jconsole
- 用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具
- 打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行
- VisualVM:故障处理工具
- 能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
- 打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行
请谈谈你对volatile的理解
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义
保证线程间的可见性
禁止进行指令重排序
保证线程间的可见性
用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
开启这三个线程,线程1执行完,更改变量状态后线程3并不会结束,为什么呢?
禁止指令重排序
什么是指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
在变量上添加volatile,禁止指令重排序
聊一下ConcurrentHashMap
ConcurrentHashMap 是一种线程安全的高效Map集合
- 底层数据结构:
- JDK1.7底层采用分段的数组+链表实现
- JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
1.7
1.8
总结
- 底层数据结构
- JDK1.7底层采用分段的数组+链表实现
- JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
- 加锁的方式
- JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
- JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
导致并发程序出现问题的根本原因是什么
Java并发编程三大特性
- 原子性
- 可见性
- 有序性
原子性
一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
不是原子操作,怎么保证原子操作呢?
内存可见性
让一个线程对共享变量的修改对另一个线程可见
有序性
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
线程池
线程池的核心参数/线程池的执行原理
public ThreadPoolExecutor(int corePoolSize, //核心线程数目
int maximumPoolSize,//最大线程数目(核心线+救急线程的最大数目)
long keepAliveTime,//生存时间 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
TimeUnit unit,//时间单位 - 救急线程的生存时间单位,如秒、毫秒等
BlockingQueue<Runnable> workQueue,//当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
ThreadFactory threadFactory,//线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
RejectedExecutionHandler handler)//拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
``线程池的执行原理`
线程池中有哪些常见的阻塞队列
ArrayBlockingQueue
:基于数组结构的有界阻塞队列,FIFO(先进先出).LinkedBlockingQueue
:基于链表结构的有界阻塞队列,FIFO。- DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
如何确定核心线程数
- IO密集型任务
- 一般来说:文件读写、DB读写、网络请求等
- 核心线程数大小设置为2N+1
- CPU密集型任务
- 一般来说:计算型代码、Bitmap转换、Gson转换等
- 核心线程数大小设置为N+1
线程池的种类有哪些
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种
-
创建使用固定线程数的线程池
- 适用于任务量已知,相对耗时的任务
- 适用于任务量已知,相对耗时的任务
-
单线程化的线程池 唯一 任务按顺序
- 适用于按照顺序执行的任务
- 适用于按照顺序执行的任务
-
可缓存线程池
- 适合任务数比较密集,但每个任务执行时间较短的情况
- 适合任务数比较密集,但每个任务执行时间较短的情况
-
提供了“延迟”和“周期执行”功能的ThreadPoolExecutor
总结
-
newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
-
newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行
-
newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
-
newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行
为什么不建议使用Executors创建线程池
使用场景
CountDownLatch
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)
- 其中构造参数用来初始化等待计数值
- await() 用来等待计数归零
- countDown() 用来让计数减一
线程池使用场景
使用场景1 ES数据批量导入
使用场景2 数据汇总
使用场景3 异步调用
搜索记录的保存可以在线程池中开启一个线程保存
总结
- 批量导入:使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM
- 数据汇总:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
- 异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间
如何控制某个方法允许并发访问线程的数量
Semaphore [ˈsɛməˌfɔr] 信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量
通常用于那些资源有明确访问数量限制的场景,常用于限流 。
Semaphore控制方法的访问量
- 创建Semaphore对象,可以给一个容量
- acquire()可以请求一个信号量,这时候的信号量个数-1
- release()释放一个信号量,此时信号量个数+1
谈谈你对ThreadLocal的理解
ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个
线程
都分配一个独立的线程副本
从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享
基本使用
ThreadLocal的实现原理
ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离
set方法
get/remove方法
ThreadLocal的内存泄漏问题
Java对象中的四种引用类型:强引用、软引用、弱引用、虚引用
- 强引用:最为普通的引用方式,表示一个对象处于
有用且必须
的状态,如果一个对象具有强引用,则GC并不会回收
它。即便堆中内存不足了,宁可出现OOM
,也不会对其进行回收
- 弱引用:表示一个对象处于
可能有用且非必须
的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收
到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够
,一旦发现则会被回收
每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了
WeakReference
。其中key为使用弱引用
的ThreadLocal实例,value为线程变量的副本
谈谈你对ThreadLocal的理解
- ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
- ThreadLocal 同时实现了线程内的资源共享
- 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
a)调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线
程的 ThreadLocalMap 集合中
b)调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
c)调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值 - ThreadLocal内存泄漏问题
ThreadLocalMap 中的 key 是弱引用,值为强引用; key 会被GC 释放内存,关联 value 的内存并不会释放。建议主动 remove 释放 key,value