1.并发和并行
并发:同一时间应对多件事情的能力。
并行:同一时间动手做(执行)多件事情的能力。
1)并发所带来的好处
并发带来性能上的提升。提高CPU的利用率,降低系统的响应时间。
提高系统的容错能力。一个线程可以不受其他线程的干扰独立运行,如果某个线程的代码里出现了Bug,这个线程可能抛出异常退出了,这时候其他线程可以不受任何影响继续执行,不至于导致整个系统都崩溃。
方便代码的编写。让每个线程实现自己的策略。
2.同步和异步
同步:需要等待结果返回,才能继续运行
异步:不需要等待结果返回,就能继续运行
3.Java中的6种线程状态
操作系统层面定义了5种线程状态分别为:初始状态,可运行状态(就绪),运行状态,阻塞状态和终止状态。
Java中的6种线程状态分别为:NEW,RUNNABLE(可运行状态,运行状态,BIO的阻塞状态),TERMINATED,BLOCKED(获取不到锁,wait()竞争锁失败),WAITING(没有时间的等待,如需要等待另一个线程运行完,join(),wait(),LockSupport.park()),TIMED_WAITING(有时间的等待,如:join(long n),sleep(long n),wait(long n))。
4.线程安全问题实例
两个线程分别自增和自减,结果不为0的原因。上下文切换导致的指令交错访问共享资源时的线程安全问题,底层字节码分析
5.局部变量的线程安全问题
局部变量通常是线程安全的,因为局部变量通常不会产生资源共享,局部变量存放在虚拟机栈的栈帧中,而这个栈帧是由线程独享的。除非当局部变量的引用暴露时,会产生线程安全问题,如子类的重写方法中又开启了一个新的线程,这时局部变量会成为共享资源,如下图。将方法修饰符改为private不允许子类重写即可解决,类加个final修饰符不允许继承也可以解决。也可private和final修饰符在并发中的作用。
6.Synchronized锁优化
锁主要存在四种状态,无锁状态、偏向锁、轻量级锁、重量级锁(Monitor),会随着竞争的强度逐渐升级,效率也会逐渐降低,其中锁可以升级但是不能降级。
重量级锁也就是将对象与Monitor关联,主要依赖操作系统的Mutex Lock互斥量来实现的,在线程切换时效率比较低。
轻量级锁通常发生在竞争并不激烈且多个线程交替时占用锁时,通过使用CAS操作且避免互斥量来提高效率,当多次自旋操作都没能获得锁时会升级为重量级锁。
偏向锁通常发生在不存在竞争且一个线程多次占用锁时,通过减少CAS操作来提高效率,当发生上下文切换时会升级为轻量级锁。
7.sleep和wait的区别
1)sleep是Thread的方法,而wait是Object的方法。
2)sleep不需要强制和synchronize配合使用,而wait需要和synchronize配合使用,也就是说需要获得锁后才能使用,notify也是如此。
3)sleep不会释放锁,而wait会释放锁。
4)共同点:调用后进入的线程状态都TIMED_WAITING。
8.volatile原理
volatile的底层实现原理是内存屏障,对volatile的写指令后会加入写屏障,对volatile的读指令前会加入读屏障。
1)如何保证可见性?
写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存中。
读屏障保证在该屏障之后的,对共享变量的读取,加载的是内存中的最新数据。
2)如何保证有序性?
写屏障保证在指令重排序时,不会将写屏障之前的代码排到写屏障之后。
读屏障保证在指令重排序时,不会将读屏障之后的代码排到读屏障之前。
9.CAS缺点
1)在高并发的情况下,共享变量一直被修改,会不断的循环判断,导致开销比较大。
2)只能保证一个共享变量的原子操作。
3)导致ABA问题(脏读),在将期望值与内存中的值比较时,虽然值相同,但是内存中的值实际已经修改了多次。虽然CAS能成功,但是内存中的值在中间是被修改过的。解决方法:用带有版本号的AtomicStampedReference。
10.线程安全的List集合类
1)new Vector<>();
2) Collections.synchronizedList(new ArrayList<>());
3) new CopyOnWritedArrayList(),在添加元素时,也就是写的时候,先进行加锁,然后将容器数组Object[]复制一份,然后在新数组添加元素,添加完成后,将原容器的引用指向新容器。而在读的时候不需要加锁,这是一种读写分离的思想。
11.伪共享
当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
当一个线程在对 a 进行修改,另一个线程在对 b 进行读取。这时候a和b通常会同时加载到缓存中的缓存行,更新完 a 后其它所有包含 a 的缓存行都将失效,因为其它缓存中的 a 不是最新值了。而当后者读取 b 时,发现这个缓存行已经失效了,需要从主内存中重新加载。
解决方法:1)让不同线程操作的对象处于不同的缓存行。2)使用编译指示,强制使每一个变量对齐,一个缓存行只有一个可操作对象。
12.ThreadLocal的内存泄漏问题
ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。所以,如果ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。这样⼀来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null 的记录。使⽤完ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法。
13.线程池中的一个线程异常了会被怎么处理?
1.execute方法,可以看异常输出在控制台,而submit在控制台没有直接输出,必须调用Future.get()方法时,可以捕获到异常。
2.一个线程出现异常不会影响线程池里面其他线程的正常执行。
3.线程不是被回收而是线程池把这个线程移除掉,同时创建一个新的线程放到线程池中。
14.如何计算 ConcurrentHashMap Size
1.JDK1.7 和 JDK1.8 对 size 的计算是不一样的。 1.7 中是先不加锁计算三次,如果三次结果不一样在加锁。
2.JDK1.8 size 是通过对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 遍历 CounterCell 数组得出 size。
3.JDK 8 推荐使用mappingCount 方法,因为这个方法的返回值是 long 类型,不会因为 size 方法是 int 类型限制最大值。