请参考下面这篇文章,总结的非常好。
面渣逆袭:Java并发六十问,快来看看你会多少
并发这里主要从下面这几个大块:
1、基础
2、ThreadLocal
3、Java内存模型
4、锁
5、并发工具类
6、线程池
7、并发容器和框架
基础
TODO
ThreadLocal
- ThreadLocal 内存泄漏是怎么回事?
- 栈中存储:ThreadLocal,Thread的弱引用
- 堆中存储:ThreadLocal、ThreadLoaclMap 的实例
- jvm垃圾回收机制运行:弱引用,不管jvm内存空间是否足够,都会回收该对象的内存。
- 内存泄漏:ThreadLocalMap 的key,即TjreadLocal的弱引用被垃圾回收器回收,而ThreadLocalMap 的生命周期是Thread是一样的。如果value此时没有被回收,则会造成内存泄漏。
- 如何解决:每次使用完ThreadLocal后,及时调用remove()方法,释放内存空间
- 那为什么ThreadLocal key还要设计成弱引用?
原因:防止内存泄漏
疑问TODO:假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLoca的强引用就没有了,但是此时key还强引用指向ThreadLoca,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。
- ThreadLocalMap的结构了解吗?
元素数组:
- 底层是个Entry数组;Entry 是个k,v 结构
- 一个table数组,存储Entry类型的元素,Entry是ThreaLocal弱引用作为key,Object作为value的结构。
散列方法:
- 目的:把key映射成table数组的下标
- 算法:哈希取余法
- 算法实现:int i = key.threadLocalHashCode & (table.length - 1);
- threadLocalHashCode:每创建一个ThreadLocal对象,就会新增:0x61c88647。这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
- ThreadLocalMap怎么解决Hash冲突的?
- 方法:开放寻址法
- 思想:简单来说,就是这个坑被人占了,那就接着去找空着的坑。i = i+1
- ThreadLocalMap扩容机制了解吗?
set—> rehash(): 1. 清理过期Entry,2. resize()
扩容阈值:theshild = (len*2)/3
决定是否扩容:size >= threshold - threshold /4
resize()
扩容过程:
- 创建2倍大小新数组
- 老数组元素散列到新数组
- table 引用指向新数组
- ThreadLocal 是怎么实现的?
- Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
- ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
- 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
- ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。
- 你在工作中用到过ThreadLocal吗?
- 做用户信息上下文的存储
- 数据库连接池的实现,也用到了TreadLocal:数据库连接池的连接交给ThreadLoca进行管理,保证当前线程的操作都是同一个Connnection。
- ThreadLocal是什么?
- 线程本地变量
- 你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
- 父子线程怎么共享数据?
父线程能用ThreadLocal来给子线程传值吗?毫无疑问,不能。那该怎么办?
这时候可以用到另外一个类——InheritableThreadLocal。
。 使用方法:主线程中对InheritableThreadLocal设置值,在子线程中可以获取到值。
2. 原理:Thread类里还有另外一个变量:ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在Thread.init的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals。
Java内存模型
- 说一下你对Java内存模型(JMM)的理解?
是什么?
1.
- 说说你对原子性、可见性、有序性的理解?
- 原子性:一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行
- 可见性:一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改。
- 有序性:有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。
- 代码分析原子性?
int i = 2;// 基本类型赋值,是原子性操作。
int j = i; // 先读i的值,再赋值到j,两步操作,不能保证原子性。
i++; //先读取i的值,再+1,最后赋值到i,三步操作了,不能保证原子性
i = i + 1;// 同上
- 原子性、可见性、有序性都应该怎么保证呢?
原子性:JMM只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用synchronized。
可见性:Java是利用volatile关键字来保证可见性的,除此之外,final和synchronized也能保证可见性。
有序性:synchronized或者volatile都可以保证多线程之间操作的有序性。
- 那说说什么是指令重排?
重排序目的: 提高程序执行的性能
谁会做:编译器、处理器
重排序分类:
- 编译器优化的重排序
- 指令级并行的重排序
- 内存系统的重排序
经典指令重拍需例子:双重校验单例模式。
对应的JVM指令分为三步:
- 分配内存空间–>
- 初始化对象—>
- 对象指向分配的内存空间
但是经过了编译器的指令重排序,第二步和第三步就可能会重排序。 通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
- 指令重排有限制吗?happens-before了解吗?
有限制:通过两个规则happens-before和as-if-serial来约束
- as-if-serial又是什么?单线程的程序一定是顺序的吗?
意思:不论如何重排序,单线程程序的执行结果不能被改变。
- volatile实现原理了解吗?
作用:有2个,保证可见性;保证有序性
问题01:volatie如何保证可见性?
- volatile可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。
问题02:volatie如何保证有序性?
- 重排序可以分为编译器重排序和处理器重排序,valatile保证有序性,就是通过分别限制这两种类型的重排序。
如何限制呢?
写:写操作前:StormStore屏障,写操作后:StoreLoad屏障
读:读操作前:LoadLoad屏障,读操作后:LoadStore屏障