面试总结之并发编程

编程入门 行业动态 更新时间:2024-10-27 09:48:26

面试总结之并发编程

面试总结之并发编程

一、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只能实现非公平锁(公平锁:先等待的线程先获取锁)
    // ReentrantLock默认是非公平锁public ReentrantLock() {sync = new NonfairSync();}// ...// 构造函数中fair为true,则创建公平锁;fair为false,则为非公平锁public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
    • ReentrantLock借助

更多推荐

面试总结之并发编程

本文发布于:2023-12-06 14:32:56,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1667873.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!