并发常见面试题进阶
1.synchronized
synchronized关键字解决的是多线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能被一个线程执行。
监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock来实现的,java的线程是映射到操作系统原生线程之上。如果要挂起一个线程,或者唤醒一个线程,都需要os来帮忙,而os实现线程切换需要从用户态转换为内核态。需要时间长,成本高。
java6之后,synchronized有了较大优化,如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量级锁。
优化来减少锁操作的开销。
2.如何使用synchronized关键字?
1.修饰实例方法:作用于当前对象实例加锁,进入同步方法需要获得对象的锁。
synchronized void method(){
//业务代码
}
2.修饰静态方法,也就是给类对象加锁。
synchronized static void method(){
//业务代码
}
3.修饰代码块,指定加锁对象,可以是当前实例对象,可以是当前类对象,可以是其他对象
synchronized(this){
//业务代码
}
synchronized(Hello.class){
//业务代码
}
synchronized(obj){
//业务代码
}
4.双重校验锁实现对象单例
public Singleton{
private volatile static Singleton uniqueInstance;
private Singleton(){
}
public static Singleton getUniqueInstance(){
if(uniqueInstance == null){
synchronized(Singleton.class){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
//uniqueInstance = new Singleton();分三步执行
//1.分配内存空间
//2.初始化内存空间
//3.将uniqueInstance指向分配的内存地址
//由于jvm可能会将指令重排,导致顺序可能变为132,已经指向了地址,但是还没初始化完毕。
//其他线程坑就直接拿去用了,导致错误。
//使用volatile可以禁止指令重排序。
3. synchronized的底层原理
3.1同步语句块的实现使用的是monitorenter
和monitorexit
指令包裹起来。
执行monitorenter
时,线程试图获取对象监视器monitor
的持有权。获取之后,锁计数器+1
执行monitorexit
时,锁计数器为0,表明锁被释放。
synchronized
修饰方法,方法的标识会有ACC_SYNCHRONIZED
本质都是对对象监视器monitor
的获取
4. jdk1.6之后,synchronized的优化
偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
偏向锁->轻量级锁->重量级锁
先是无锁状态,有一个线程需要使用锁资源就用偏向锁,具体是在MarkWord里设置了本线程的线程id,以后该线程
进入和推出都不用加锁和解锁。只需要简单测试下锁资源的对象头的MarkWord里面有没有自己的id就行了。
如果有线程争用资源,就进化为轻量级锁,markword里面的数据标识为00(轻量级锁标识),此时只能有一个线程获得锁,其他线程自旋,自旋一定时间还没有获得锁,那锁资源的对象头的Markword就会被修改为重量级锁,标识为10,然后没获得线程的就挂起,剩下的新来的线程一看到是重量级锁,也直接挂起。
偏向锁适用于只有一个线程访问的同步场景,就是这个场景需要用同步保证安全,但是一般并发数不高。
轻量级锁是竞争不激烈的时候,挂起与唤醒都太消耗资源,所以就用自旋的方式解决。
如果自旋一直拿不到,那就说明竞争还是激烈,就不自旋浪费cpu了,直接挂起,改为重量级锁。
5.synchronized和ReentrantLock的区别
1.都是可重入锁,自己可以再次获取自己的内部锁,同一个线程每次获得锁,锁计数器都+1。到0才算释放。
2.synchronized依赖于JVM,ReentrantLock依赖于API,需要lock()和unlock()配合try/finally语句块来实现。
3.ReentrantLock比synchronized增加的功能:
- 等待可中断:通过lock.lockInterruptibly()来实现。正在等待的线程可以放弃等待,改为处理其他事情。
- 可实现公平锁:synchronized只能是非公平锁。公平锁:先来先得。ReentrantLock(boolean fair)实现。
- 可实现选择性通知:synchronized的wait()和notify/notifyAll()可以实现等待/通知机制。但是只能通知一个或所有。而ReentrantLock可以借助Condition接口实现多路通知。
- 创建多个Condition实例,线程可以注册到指定的Condition中,这样signalAll()只会唤醒注册在该Condition实例中的多有等待线程。
6.volatile 关键字
6.1 Java内存模型 JMM
线程可以把变量保存在本地内存,比如机器的寄存器。而不是直接在在主存中进行读写。这造成一个线程在主存中修改了一个变量的值,另一个线程还在用寄存器中的变量值拷贝。造成数据不一致。
volatile
关键字指示JVM这个变量是共享且不稳定的,每次都要从主存中读取。
所以volatile
除了防止指令重排
,还能保证变量的可见性
。
并发编程的三个重要特性
- 原子性:和数据库原子性类似,不解释。synchronized可以做到原子性。
- 可见性:当一个变量对共享变量进行了修改,另外的线程都是立即可以看到修改后的最新值。volatile保证共享变量的可见性。synchronized也可以保证可见性。
- 有序性:代码执行过程中,顺序可能会被jvm打乱。volatile可以禁止指令重排。比如在双重check单例模式的懒汉模式。
7.ThreadLocal
7.1 结构与用途
实现每一个线程都有自己的专属本地变量。
如果创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本。
public class Thread implements Runnable{
//与此线程有关的ThreadLocal值,由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
}
//ThreadLocal 的set方法
public void set(T value){
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if(map!=null)
map.set(this,value);
else
createMap(t,value);
}
ThreadLocalMap getMap(Thread t)
return t.threadLocals;
//变量的实际存储位置就是ThreadLocalMap,ThreadLocal为key,通过访问ThreadLocal变量这个key,去当前线程找到了threadLocalMap里面的Entry,用key取出值。所以是线程隔离的。
7.2 Threadlocal内存泄漏问题
ThreadLocal
在Java中是一个比较特殊的类,它提供了一种线程局部变量的概念,即每个使用该变量的线程都有这个变量的一个独立副本,互不干扰。在并发编程中,ThreadLocal
是解决线程安全问题的一个常用工具。
然而,ThreadLocal
的使用如果不当,确实可能会导致内存泄漏问题。内存泄漏通常发生在使用ThreadLocal
的线程结束后,ThreadLocal
中保存的对象没有被回收。
为什么会发生内存泄漏?
内存泄漏通常是指应用程序不再需要的内存,由于某种原因没有被操作系统或可用内存回收机制回收,导致该部分内存始终得不到释放。在ThreadLocal
的情况下,内存泄漏的原因通常与其内部使用的ThreadLocalMap
有关,这是一个专门为每个线程维护的ThreadLocal
变量的复制的数据结构。
-
ThreadLocalMap的键是对ThreadLocal对象的弱引用
ThreadLocalMap
使用ThreadLocal
实例作为键,而这些键是弱引用。如果没有外部强引用指向ThreadLocal
实例,那么在垃圾回收时,ThreadLocal
实例可能会被回收。 -
ThreadLocalMap的值对Entry对象的引用是强引用
即使
ThreadLocal
实例被回收,但是ThreadLocalMap
中的值(也就是用户保存的对象)仍然持有强引用,这意味着即使ThreadLocal
的键被清理了,值仍旧不会被垃圾回收器回收。 -
线程生命周期
如果是线程池中的线程,那么线程可能会长时间存在,即便执行的任务已经结束。这就意味着,
ThreadLocalMap
中的值可能会长时间保留,直到线程结束。
如何避免内存泄漏?
要防止因为ThreadLocal
使用不当而导致的内存泄漏,可以采取以下措施:
-
及时清除
在不再需要访问
ThreadLocal
存储的数据时,应该显式调用ThreadLocal
的remove()
方法来清除ThreadLocalMap
中的相关条目。threadLocal.remove();
-
尽量不在
ThreadLocal
中存放大的对象或者容易导致内存泄漏的对象如果确实需要存放大的对象或者容易导致内存泄漏的对象,要确保在不使用时能够及时地调用
remove()
。 -
使用完毕后结束线程
对于不是池化的线程,确保其执行完毕后能够及时结束。对于线程池中的线程,确保在关闭线程池时清理线程本地变量。
-
小心内部类的引用
如果你在一个内部类中使用了
ThreadLocal
,那么要注意是否有可能无意中持有了对外部类的引用。
通常,只要在使用ThreadLocal
时遵循良好的编程习惯,就能够有效避免内存泄漏问题。在需要长期运行的线程,尤其是线程池场景中,务必注意对ThreadLocal
变量的正确管理。
7.3 ThreadLocal实战应用
在微服务架构中,前端访问服务A,A访问B,然后为