面试总结之并发编程
一、ThreadLocal
1、什么是ThreadLocal
- ThreadLocal是一种多线程隔离机制,提供了多线程环境下对共享变量访问的安全性
- 在多线程访问共享变量的场景中(如上图),一般的解决方案是对共享变量加锁,从而保证同一时刻只有一个线程能对共享变量进行更新(如下图),并且基于Happens-Before原则中的监视器锁规则,又保证了数据修改后对其他线程的可见性
- 加锁会带来性能的下降,ThreadLocal采取了一种空间换时间的思路,在每个线程中都用容器来存储共享变量的副本,每个线程只对自己的变量副本进行访问和操作,如此,既解决了线程安全问题,又避免多线程竞争锁的开销
- ThreadLcoal实现原理:Thread类中有成员变量ThreadLcoalMap,用来专门存储当前线程共享变量的副本,后续当前线程对共享变量的操作,都基于ThreadLcoalMap来进行,不会影响全局共享变量的值
2、ThreadLocal在项目中的实际应用
- 在典型的MVC系统架构中,登录后的用户每次访问接口,都会在请求头中携带一个Token,在控制层可以根据该Token解析出登录用户的基本信息,那如果要在服务层和持久层都要用到登录用户的信息,如RPC调用,更新用户信息等,那要该如何实现?
- 这时就可以使用ThreadLcoal,在控制层拦截请求,将用户信息存储到ThreadLocal,如此,就可以在服务层和持久层获取到ThreadLcoal中存储的登录用户信息
public class UserHolder {private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();public static void saveUser(UserDTO user){tl.set(user);}public static UserDTO getUser(){return tl.get();}public static void removeUser(){tl.remove();}
}
- 使用:
// 获取当前的登录用户idLong userId = UserHolder.getUser().getId();
扩展:
- 其他场景的cookie、session等数据隔离的操作都可以通过ThreadLcoal实现
- 数据库连接池中的connection连接交给ThreadLocal来管理,保证当前线程操作的都是同一个connection
3、ThreadLocal实现原理
-
每个线程都有一个成员变量ThreadLocalMap,当线程访问ThreadLocal修饰的共享数据时,该线程就会在自己的ThreadLocalMap中存储一份共享数据的副本,key指向ThreadLocal这个弱引用,value保存的是共享数据的副本,因为每个线程都有一份共享数据的副本,以此就解决了线程安全问题
-
ThreadLocal的set方法:
public void set(T value) {// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t);// 将当前元素存入ThreadLocalMap if (map != null)map.set(this, value);elsecreateMap(t, value);}// ....ThreadLocalMap getMap(Thread t) {return t.threadLocals;}// ...void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
- ThreadLocal实现的关键在于ThreadLocalMap,Thread类中定义了ThreadLocal.ThreadLocalMap类型的成员变量threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
- map本质上是一个个<key,value>键值对形式的节点组成的数组,那ThreadLcoalMap的节点是什么样的呢
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;// 节点的构造方法Entry(ThreadLocal<?> k, Object v) {// key赋值super(k);// value赋值value = v;}}
- 这里Entry节点中的key可以看作是ThreadLocal的弱引用,value为向ThreadLocal中存储的值,Entry的key继承了WeakReference
小结:
实现ThreadLocal的关键点:
- Thread类中有ThreadLocal.ThreadLocalMap类型的实例变量,每个线程都有自己的ThreadLocalMap,ThreadLocalMap内部维护着Entry数组,每个Entry都代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值
- ThreadLocal本身不存储key,只是作为key来让线程往ThreadLocalMap中存取值
- 每个线程在往ThreadLocal中设置值时,都是往线程自己的ThreadLocalMap中存值,取值也是以某个ThreadLocal类型的key作为引用,在线程自己的map中查找对应的key,以此来实现线程隔离
4、ThreadLocal内存泄露
- 在JVM中,栈内存线程私有,存储对象的引用,堆内存线程共享,用来存储对象实例 ==》 栈存储了ThreadLocal、Thread的引用,堆存储了ThreadLocal和Thread对象的具体实例
- 当JVM发生GC后,会断开Entry中的key到ThreadLocal对象中的引用(key为弱引用),key为null,value为强引用不会为null,整个Entry不会为null,会依然在ThreadLocalMap中占据内存,当通过ThreadLocal的get方法获取数据时,ThreadLocal并不为null,但也无法通过为null的key去访问到该Entry的value,如此就会造成内存泄露(占据内存也无法访问到)
如果key为强引用是否会造成内存泄露
可以先看如下代码:
ThreadLocal threadLocal = new ThreadLocal();threadLocal.set(new Object());threadLocal = null;
- 在set方法执行完后,直接将threadLocal设为null,此时栈中Thread的引用到堆中ThreadLocal对象的指向断开了,但是Entry中的key到ThreadLocal的引用依然存在,GC依旧无法回收,同样会造成内存泄露
- key为弱引用比强引用好在哪:
- 同样是如上代码,当key为弱引用,threadLocal设为null时,栈中ThreadLocal Reference到堆中ThreadLocal的指向断开,Entry到threadLocal的指向也会断开,此时threadLocal就会被回收
- ThreadLocal也会根据key.get() == null来判断key是否被回收,ThreadLocal可自行清理这些过期的key来避免内存泄露
5、父子线程如何共享数据
- 父线程不能用ThreadLocal来给子线程传值,父子线程之间的数据共享需要通过InheritableThreadLocal来实现,即在主线程的InheritableThreadLocal实例设置值,在子线程中就可以获取到设置的值
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
// ThreadLocal<String> threadLocal = new ThreadLocal<>();// 向主线程中的threadLocal设值threadLocal.set("世界上最好的编程语言");// 子线程Thread sonThread = new Thread(){@Overridepublic void run() {
// super.run();System.out.println(threadLocal.get() + " 是Java");}};sonThread.start();
在Thread类中,有 ThreadLocal.ThreadLocalMap类型的成员变量threadLocals和inheritableThreadLocals:
ThreadLocal.ThreadLocalMap threadLocals = null;// ...ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在Thread的init()方法中,如果父线程的 inheritableThreadLocals 不为空,就把它赋给当前线程(子线程)的 inheritableThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/* Stash the specified stack size in case the VM cares */this.stackSize = stackSize;/* Set thread ID */tid = nextThreadID();// ....
public class InheritableThreadLocal<T> extends ThreadLocal<T> {/*** Computes the child's initial value for this inheritable thread-local* variable as a function of the parent's value at the time the child* thread is created. This method is called from within the parent* thread before the child is started.* <p>* This method merely returns its input argument, and should be overridden* if a different behavior is desired.** @param parentValue the parent thread's value* @return the child thread's initial value*/protected T childValue(T parentValue) {return parentValue;}/*** Get the map associated with a ThreadLocal.** @param t the current thread*/ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}/*** Create the map associated with a ThreadLocal.** @param t the current thread* @param firstValue value for the initial entry of the table.*/void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}
扩展、Java四种对象引用
- 强引用:程序代码中普通存在的赋值行为,如:Object obj = new Object(); 只要强引用关系还在,对象就永远不会被回收
- 软引用:不是必须存活的对象,JVM会在内存溢出之前进行回收(即内存满了才会进行回收),如:缓存
- 弱引用:引用关系比软引用还弱,不管JVM内存是否够用,都会回收对象占用的内存
- 虚引用:又称为"幽灵引用"、“幻影引用”,是最弱的引用关系,完全不会影响对象的回收,唯一的作用是对象被回收时收到一个系统通知
二、Java内存模型
三、锁
1、synchronized
synchronized可以用来修饰实例方法、静态方法、代码块,以保证程序代码的原子性
- synchronized修饰实例方法:进入同步代码前要获得当前对象实例的锁
synchronized void method(){// ...}
- synchronized修饰静态方法:给当前类加锁,作用于类的所有对象实例,进入同步代码前要先获得class的锁,因为静态成员不属于任何一个实例对象,属于类成员(static声明这是该类的静态资源,不管new了多少个对象,只有一份)
如果线程A调用某实例对象的非静态同步方法,而线程B调用该实例对象所属类的静态同步方法,这种情况会被允许,不会发生互斥现象,因为访问静态同步方法占用的锁是当前类的锁,而访问非静态同步方法占用的锁是当前实例对象的锁
synchronized static void method() {// ...
}
- synchronized修饰代码块:指定加锁对象,对给定的类/ 对象加锁,synchronized(this) 或synchronized(object) 表示进入同步代码块前,要先获得给定对象的锁,synchronized(类名.class)表示进入同步代码块前要获得当前class的锁
synchronized (Person.class) {// ...
}
2、synchronized的实现原理
-
当我们使用synchronized时,JVM会自动进行lock和unlock操作
-
synchronized修饰代码块时,JVM采用monitorenter、monitorexit两个指令来实现同步,monitorenter指向同步代码块的开始位置(lock操作),第一个monitorexit指向同步代码块的结束位置(unlock操作),第二个monitorexit保证出现异常也能unlock
-
synchronized修饰代码块时,采用ACC_SYNCHRONIZED来标识该方法是一个synchronized修饰的同步方法
-
monitorenter、monitorexit和ACC_SYNCHRONIZED都是基于Monitor对象实现的
-
实例对象结构中有对象头,对象头中MarkWord指针会指向Monitor,Monitor是一种同步工具 / 同步机制,在Java虚拟机(Hotspot)中,Monitor由ObjectMonitor实现,又称为内部锁,或者Monitor锁
-
Monitor的工作原理:
- ObjectMonitor有两个队列:WaitSet、EntryList,用来保存ObjectWaiter对象列表
- _owner:获取Monitor对象的线程进入_owner区时,_count+1;如果线程调用了wait()方法,就会释放Monitor对象,_owner为空,_count-1,同时该等待线程进入_WaitSet中,等待被唤醒
ObjectMonitor() {_header = NULL ;_count = 0 ; // 记录 线程获取锁的次数_waiters = 0 ,_recursions = 0 ; / /锁 的 重 ⼊ 次 数_object = NULL ;_owner = NULL ; // 指向持 有ObjectMonitor对 象 的线程_WaitSet = NULL ; // 处 于wait状 态 的线程,会被 加 ⼊ 到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ; // 处 于 等 待 锁block状 态 的线程,会被 加 ⼊ 到 该 列 表_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;}
去医院就诊的过程就和Monitor比较类似:
- 门诊大厅(EntrySet):所有待进入的线程都必须先在EntrySet挂号才有资格就诊
- 就诊室(_owner):_owner中只能有一个线程就诊,就诊完毕线程就自行离开
- 候诊室(WaitSet):就诊室繁忙时,进入等待区(WaitSet),就诊室空闲时就从等待区(WaitSet)叫醒等待就诊的线程
小结: - monitorenter:在判断拥有同步表示ACC_SYNCHRONIZED后,抢先进入该同步方法的线程会优先拥有Monitor的owner,此时计数器+1
- monitorexit:当执行完退出后,计数器-1,计数器归零后被其他进入的线程获取
- 基于Monitor中的计数器,Monitor可以记录锁重入的次数(线程获取锁的次数)
3、synchronized可见性、有序性、可重入性
-
可见性实现:
- 线程加锁前,将清空工作内存中共享变量的值,在使用共享变量时需要从主内存中重新读取最新的值
- 线程加锁后,其他线程不被允许读取主内存中的共享变量
- 线程解锁前,必须将工作内存中共享变量的值刷新到主内存中
-
有序性实现:
- synchronized修饰的同步代码,具有排他性,一次只能被一个线程持有 ==》synchronized保证同一时刻,程序是单线程执行的
- 由于as-if-serial语义的存在,synchronized保证的是执行结果的有序性,而非防止指令重排的有序性
-
可重入性:
- synchronized允许一个线程多次请求自己持有对象锁的临界资源,所以是可重入锁
- synchronized锁对象时通过计数器记录下线程获取锁的次数,每获取一次锁(monitorenter),计数器+1,执行完对应的同步代码后,即每释放一次锁(monitorexit),计数器-1,直到计数器归零后,就表示可重入的代码已经执行完,最外层的锁会释放
4、锁升级 & synchronized优化 后续整理
未完待续…
5、synchronized和ReentrantLock的区别
可以从锁的实现、性能、功能特点等多个维度去回答该问题:
- 实现:synchronized是Java的关键字,基于JVM实现;ReentrantLock基于JDK的API来实现,是Lock的实现类(lock方法和unlock方法配合try-finally语句块来实现加锁、释放锁)
- 性能:JDK1.6锁优化以前,synchronized性能比ReentrantLock相差较多,但JDK1.6以后,增加了适应性自旋、锁消除以后,性能相差无几
- 功能特性:对比synchronized,ReentrantLoc多了一些高级特性,如:等待可中断、可实现公平锁、条件通知
- ReentrantLoc提供了一种能够中断等待锁的线程的机制,通过
lock.lockInterruptibly();
来实现,而synchronized不能实现 - ReentrantLock可以实现公平锁和非公平锁,而synchronized只能实现非公平锁(公平锁:先等待的线程先获取锁)
- ReentrantLoc提供了一种能够中断等待锁的线程的机制,通过
// ReentrantLock默认是非公平锁public ReentrantLock() {sync = new NonfairSync();}// ...// 构造函数中fair为true,则创建公平锁;fair为false,则为非公平锁public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
-
- ReentrantLock借助
更多推荐
面试总结之并发编程
发布评论