1、创建子线程时,子线程是得不到父线程的 ThreadLocal,有什么办法可以解决这个问题?
可 以 使 用 InheritableThreadLocal 来 代 替 ThreadLocal , ThreadLocal 和
InheritableThreadLocal 都是线程的属性,所以可以做到线程之间的数据隔离,在多线程环境下我们经常使用,但在有子线程被创建的情况下,父线程 ThreadLocal 是无法传递给子线程的,但 InheritableThreadLocal 可以,主要是因为在线程创建的过程中,会把InheritableThreadLocal 里面的所有值传递给子线程,具体代码如下:
// 当父线程的 inheritableThreadLocals 的值不为空时
// 会把 inheritableThreadLocals 里面的值全部传递给子线程
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)
2、CopyOnWriteArrayList 通过哪些手段实现了线程安全?
答:主要有:
- 数组容器被 volatile 关键字修饰,保证了数组内存地址被任意线程修改后,都
会通知到其他线程;- 对数组的所有修改操作,都进行了加锁,保证了同一时刻,只能有一个线程对数组进行修改,比如我在 add 时,就无法 remove;
- 修改过程中对原数组进行了复制,是在新数组上进行修改的,修改过程中,不会对原数组产生任何影响。
3、在 add 方法中,对数组进行加锁后,不是已经是线程安全了么,为什么还需要对老数组进行拷贝?
答:的确,对数组进行加锁后,能够保证同一时刻,只有一个线程能对数组进行 add,在同单核 CPU 下的多线程环境下肯定没有问题,但我们现在的机器都是多核 CPU,如果我们不通过复制拷贝新建数组,修改原数组容器的内存地址的话,是无法触发 volatile 可见性效果的,那么其他 CPU 下的线程就无法感知数组原来已经被修改了,就会引发多核 CPU 下的线程安全问题。
假设我们不复制拷贝,而是在原来数组上直接修改值,数组的内存地址就不会变,而数组被volatile 修饰时,必须当数组的内存地址变更时,才能及时的通知到其他线程,内存地址不变,仅仅是数组元素值发生变化时,是无法把数组元素值发生变动的事实,通知到其它线程的。
4、 ConcurrentHashMap 通过哪些手段保证了线程安全。
答:主要有以下几点:
- 储存 Map 数据的数组被 volatile 关键字修饰,一旦被修改,立马就能通知其他线程,因为是数组,所以需要改变其内存值,才能真正的发挥出 volatile 的可见特性;
- put 时,如果计算出来的数组下标索引没有值的话,采用无限 for 循环 + CAS 算法,来保证一定可以新增成功,又不会覆盖其他线程 put 进去的值;
- 如果 put 的节点正好在扩容,会等待扩容完成之后,再进行 put ,保证了在扩容时,老数组的值不会发生变化;
- 对数组的槽点进行操作时,会先锁住槽点,保证只有当前线程才能对槽点上的链表或红黑树进行操作;
- 红黑树旋转时,会锁住根节点,保证旋转时的线程安全。
5、描述一下 CAS 算法在 ConcurrentHashMap 中的应用?
答:CAS 其实是一种乐观锁,一般有三个值,分别为:赋值对象,原值,新值,在执行的时候,会先判断内存中的值是否和原值相等,相等的话把新值赋值给对象,否则赋值失败,整个过程都是原子性操作,没有线程安全问题。
ConcurrentHashMap 的 put 方法中,有使用到 CAS ,是结合无限 for 循环一起使用的,
步骤如下:
- 计算出数组索引下标,拿出下标对应的原值;
- CAS 覆盖当前下标的值,赋值时,如果发现内存值和 1 拿出来的原值相等,执行赋值,退出循环,否则不赋值,转到 3;
- 进行下一次 for 循环,重复执行 1,2,直到成功为止。可以看到这样做的好处,第一是不会盲目的覆盖原值,第二是一定可以赋值成功。
6、 ConcurrentHashMap 是如何发现当前槽点正在扩容的。
答:ConcurrentHashMap 新增了一个节点类型,叫做转移节点,当我们发现当前槽点是转移节点时(转移节点的 hash 值是 -1),即表示 Map 正在进行扩容。
7、 发现槽点正在扩容时,put 操作会怎么办?
答:无限 for 循环,或者走到扩容方法中去,帮助扩容,一直等待扩容完成之后,再执行 put操作。
8、 两种 Map 扩容时,有啥区别?
答:区别很大,HashMap 是直接在老数据上面进行扩容,多线程环境下,会有线程安全的问题,而 ConcurrentHashMap 就不太一样,扩容过程是这样的:
- 从数组的队尾开始拷贝;
- 拷贝数组的槽点时,先把原数组槽点锁住,拷贝成功到新数组时,把原数组槽点赋值为转移节点;
- 从数组的尾部拷贝到头部,每拷贝成功一次,就把原数组的槽点设置成转移节点;
- 直到所有数组数据都拷贝到新数组时,直接把新数组整个赋值给数组容器,拷贝完成。
简 单 来 说 , 通 过 扩 容 时 给 槽 点 加 锁 , 和 发 现 槽 点 正 在 扩 容 就 等 待 的 策 略 , 保 证 了ConcurrentHashMap 可以慢慢一个一个槽点的转移,保证了扩容时的线程安全,转移节点比较重要,平时问的人也比较多。
9、 ConcurrentHashMap 在 Java 7 和 8 中关于线程安全的做法有啥不同?
答:非常不一样,拿 put 方法为例,Java 7 的做法是:
- 把数组进行分段,找到当前 key 对应的是那一段;
- 将当前段锁住,然后再根据 hash 寻找对应的值,进行赋值操作。
Java 7 的做法比较简单,缺点也很明显,就是当我们需要 put 数据时,我们会锁住改该数据对应的某一段,这一段数据可能会有很多,比如我只想 put 一个值,锁住的却是一段数据,导致这一段的其他数据都不能进行写入操作,大大的降低了并发性的效率。Java 8 解决了这个问题,从锁住某一段,修改成锁住某一个槽点,提高了并发效率。不仅仅是 put,删除也是,仅仅是锁住当前槽点,缩小了锁的范围,增大了效率。
10、快速失败(fail—fast)
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
11、安全失败(fail—safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
12、Java有一个happens-before原则:
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
2、锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作。必须保证先写再读。
4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,那么可以得出操作A先行发生于操作C。
5、线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生。
7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测现场是否已经终止执行
8、对象终结规则:一个对象的初始完成先行发生于他的finalize()方法的开始。
13、volatile底层是如何基于内存屏障保证可见性和有序性的?
volatile+原子性:不能够保证原子性,只有在一些极端情况下能保证原子性。
保证原子性:synchronized,lock,加锁。
(1)lock指令:volatile保证可见性
对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改
如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了
lock前缀指令 + MESI缓存一致性协议
(2)内存屏障:volatile禁止指令重排序
volatille是如何保证有序性的?加了volatile的变量,可以保证前后的一些代码不会被指令重排,这个是如何做到的呢?指令重排是怎么回事,volatile就不会指令重排,简单介绍一下,内存屏障机制是非常非常复杂的,如果要讲解的很深入
Load1:int localVar = this.variable
Load2:
int localVar = this.variable2
LoadLoad屏障:Load1;LoadLoad;Load2,确保Load1数据的装载先于Load2后所有装载指令,他的意思,Load1对应的代码和Load2对应的代码,是不能指令重排的
Store1:
this.variable = 1
StoreStore屏障
Store2:
this.variable2 = 2
StoreStore屏障:Store1;StoreStore;Store2,确保Store1的数据一定刷回主存,对其他cpu可见,先于Store2以及后续指令
LoadStore屏障:Load1;LoadStore;Store2,确保Load1指令的数据装载,先于Store2以及后续指令
StoreLoad屏障:Store1;StoreLoad;Load2,确保Store1指令的数据一定刷回主存,对其他cpu可见,先于Load2以及后续指令的数据装载
volatile的作用是什么呢?
volatile variable = 1
this.variable = 2 => store操作
int localVariable = this.variable => load操作
对于volatile修改变量的读写操作,都会加入内存屏障
每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排
每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排
网友评论