并行与并发有什么区别
单核CPU
串行:一个线程执行完再执行另一个线程
并行:多个线程同时执行
由于线程切换速度快,所以宏观上是并行的
并发:类似于串行,实际上也是轮流执行的情况
多核CPU
多核的情况下可以多个线程各自使用一个CPU核心,实现真正意义上的并行,即微观上也是并行的
线程的创建方式
继承Thread类,重写run方法
实现Runnable接口,重写run方法
将Runnable对象作为参数封装给线程
实现Callable接口
需要给定一个泛型(这里以String为例),重写call方法返回的结果类型就是这个泛型,因此这种线程创建方式适用于线程有返回结果的场景,如果要获取线程的返回结果,可以使用.get()方法
具体创建线程是通过将MyCallable对象作为参数封装给FutureTask,然后又将FutureTask作为参数封装给线程
使用线程池创建线程
拓展
线程的状态
如何保证线程的顺序执行
使用join
notify()和notifyAll()的区别
JM有很多实现,比较流行的就是hotspot,hotspot对notofy0的实现并不是我们以为的随机唤醒,而是先进先出的顺序唤醒。
wait()和sleep()的区别
调用wait方法必须先加锁,不加锁则报错
wait方法执行后会释放掉锁,此时其他线程可以获得锁:如图中执行到wait方法后,锁释放了,因此下面的main.debug可以获得锁执行代码,当wait执行结束时又会打印出running...end...
sleep方法执行后不会释放锁,其他线程无法获得锁
如何停止一个正在运行的线程
使用退出标志
主线程中开启子线程,子线程执行run方法,run方法中根据flag进行循环,执行完一次打印就睡3秒钟
而主线程开启子线程后睡了6秒才更新flag标记,所以在主线程睡的6秒钟,子线程可以进行两次打印,之后由于flag被更新了,循环被打破,因此线程停止
怎么保证多线程安全
Synchronized关键字的底层原理
使用回顾
底层汇编
通过反汇编字节码文件可以发现Synchronized底层使用了monitorenter和monitorexit,需要注意的是这里有两个解锁,最后一个解锁是为了防止代码运行抛异常的时候之前上的锁没有释放掉,因此最后一个解锁起到以防万一的作用
Owner就是获取锁成功的线程,EntryList就是获取锁失败的线程(阻塞)集合,WaitSet就是调用wait方法后等待的线程集合
基础回答
进阶原理
Monitor是jvm级别的(可以理解为内核态),而用户编写的代码属于用户态,两种状态的切换导致性能很低,所以说Monitor实现的锁是重量级锁
偏向锁和轻量级锁是针对于线程没有竞争的情况设计出来的,其中偏向锁是说只有一个线程获取锁,没有竞争对手,所以这个锁只偏向于这个线程;而轻量级锁是说虽然有多个线程,但是这些线程因为某种原因(比如执行顺序),恰好没有发生竞争的情况,一个线程用完锁之后另一个线程才接着获取锁,这种没有竞争的情况下如果用重量级锁就很浪费,所以出现了轻量级锁
锁对象关联上Monitor:通过hotspot中对象的内存结构中的对象头的MarkWord里的指针
轻量级锁的执行流程:
首先Object是锁对象obj的内存结构,当第一次获取锁时,获取锁的线程会新增一个Lock Record记录,其中默认初始使用的是轻量级锁(编号为00),然后线程通过CAS(Compare And Set)操作与obj内存结构中的hashcode age进行交换,如果说CAS失败,说明有其他线程也在获取锁并修改了hashcode age(出现了竞争),此时的轻量级锁就应该升级为重量级锁
另外,如果是锁重入(如图中的代码),线程会再新增一个Lock Record记录,但这条记录的数据为null,它同样有必要进行CAS操作,但这里的锁重入其实已经修改好了CAS(或者说两次记录的CAS操作修改后是一样的)
轻量级锁的释放:
在释放锁的时候,如图中为重入锁,线程会首先按与获取锁相反的顺序释放锁,因此先看倒数的Lock Record记录,如果它的数据为null,则直接删除这条记录,如果这条记录有数据,则会与Object的数据再次交换回原来的样子,最后释放锁
偏向锁:
和轻量级锁的区别在于交换数据中有线程id,再次获取锁的时候只需要判断线程id是否是自己即可,如果是就新增Lock Record记录
进阶回答
Synchronized的使用方式
修饰代码块
多线程---同一对象
多线程---不同对象
多线程---自定义锁对象
修饰方法
修饰静态方法
修饰类
Jave内存模型(JMM)
线程内的数据存储在工作内存,不同线程的工作内存不能互相访问(及不同线程的数据不能共享),所以需要用到主内存来进行变量的同步
CAS
原理
CAS就是先比较后设值(Compare And Set),如果比较失败,则不断地循环获取被其他线程修改后的值再次进行比较,这种不断循环就是自旋,直到比较成功
由于这种自旋锁并不是真正意义上的加锁,而只是一种循环,所以不会出现线程的阻塞,效率较高,当然如果一直循环下去,也还是会影响效率
底层实现
通过操作系统底层的CAS指令实现
乐观锁、悲观锁
CAS缺点、为什么不是所有锁都是用CAS
回答
volatile的理解
理解
Java并发编程的特性之一:可见性
volatile关键字用来修饰变量,可以使变量在线程之间相互可见,即变量的变化是同步的
在没添加volatile关键字以及指定运行参数的情况下,线程间变量不可见的原因是jit多此一举地给代码做了优化
禁止指令重排序:
对变量y添加volatile关键字,写操作和读操作加屏障,防止跳序执行
对变量x添加volatile关键字则无法实现,解决方案:
即在actor1中将x放在第二行,actor2中将x放在第一行
指令重排序的原理是什么
volatile可以保证线程安全吗
volatile和sychronized比较
什么是AQS
对于AQS,内部维护了一个state变量用于记录是否有锁,如果一个线程获取锁成功就会将state置为1,此后其他线程无法获取锁,就会进入FIFO双向队列,队列中有两个指针维护了头节点和尾节点
那如果在还无锁的条件下,有多个线程同时争抢锁呢,如何保证原子性:使用CAS
AQS既可以实现公平锁也可以实现非公平锁
非公平锁吞吐量为什么比公平锁大
ReentrantLock的实现原理
ReentrantLock是怎么实现公平锁的
Synchronized和Lock有什么区别
- Lock提供了可打断,即某个线程正在获得锁时可以被打断,导致获取锁失败;提供了可超时,获得锁时如果超时就获取锁失败,以及可以在获得锁时使用条件等
死锁产生的条件
简单来说就类似于Spring的循环依赖
如何进行死锁诊断
ConcurrentHashMap
总结
并发程序出现问题的根本原因是什么(如何保证多线程的执行安全)
Java并发编程三大特性
原子性:
可见性:
通过volatile关键字加到共享变量flag上,就可以实现flag在线程之间可见,一个线程修改了flag,另一个线程同样会接收到flag的变化,如图一旦flag被置为了true,则线程中断
有序性:
在前面的volatile详解中已有提及,不再赘述
线程池的核心参数与执行原理
- 关于生存时间,主要指的是救急线程(临时线程)的生存时间,一旦过期就会释放线程资源,而核心线程永远不会被释放
- 关于阻塞队列workQueue,如果核心线程用完了,新的任务就会被放到阻塞队列,如果阻塞队列满了,就会开始使用救急线程
- 关于拒绝策略,如果连救急线程都用完了,就会触发拒绝策略
线程池中有哪些常见的阻塞队列
强制有界是指new阻塞队列时必须传入大小,默认无界,支持有界是指new阻塞队列时可以不传大小,但会默认给你设置一个大小值
如何确定核心线程数
其中N为cpu核心数
线程池的种类有哪些
由于使用的是固定线程数的线程池(核心线程数=最大线程数目,也就是无救急线程),说明所需的线程数大体上是已知的,即使不足,也不会差别很多,因此适用于任务量已知,相对耗时的任务
单线程的线程池,只使用一个线程,可以保证任务的执行顺序
可缓存的线程池,没有核心线程,全是救急线程,救急线程的存活时间为60s,又由于全是救急线程,那就没有存在阻塞队列的必要,于是使用SynchronousQueue,任务一来,就交给救急线程执行
提交的任务可以由线程池的schedule方法指定延迟或者周期执行的时间
为什么不建议用Executor而用ThreadPoolEXecutor创建线程池
根据上一题的介绍,以上三种线程池都会出现Integer.MAX_VALUE这样的参数,肯定会有问题,所以不建议使用,而是用下面的方法
项目中哪里使用到了线程池
ES数据批量导入
等所有进程倒计时结束await才会继续执行
其实就是大批量数据分为多批次数据,由多个线程进行插入到es索引库,每执行完一个插入任务,就使count减一,直到ocunt减为0才能继续执行await
数据汇总
改串行为并行
异步线程
需要异步的方法上加注解,指定线程池
引导类启动异步调用
总结
如何控制某个方法允许并发访问线程的数量
对ThreadLocal的理解
其实就是保证线程池中的线程互不干扰
其实就是每个线程各自维护一个ThreadLocalMap,而ThreadLocalMap中又有一个Entry数组用于存储数据,在set时如果这个Map不存在,说明是第一次存储数据,那么就会调用ThreadLocalMap的构造方法并构造出Entry数组,然后对数据进行一些位运算,将数据存入到数组指定的位置,而get/remove的时候就同样通过确定数据的位置获取到/删除数据