面试总结之并发编程

一、ThreadLocal

1、什么是ThreadLocal

  • ThreadLocal是一种多线程隔离机制,提供了多线程环境下对共享变量访问的安全性
    面试总结之并发编程_第1张图片
  • 在多线程访问共享变量的场景中(如上图),一般的解决方案是对共享变量加锁,从而保证同一时刻只有一个线程能对共享变量进行更新(如下图),并且基于Happens-Before原则中的监视器锁规则,又保证了数据修改后对其他线程的可见性
    面试总结之并发编程_第2张图片
  • 加锁会带来性能的下降,ThreadLocal采取了一种空间换时间的思路,在每个线程中都用容器来存储共享变量的副本每个线程只对自己的变量副本进行访问和操作,如此,既解决了线程安全问题,又避免多线程竞争锁的开销
  • ThreadLcoal实现原理:Thread类中有成员变量ThreadLcoalMap,用来专门存储当前线程共享变量的副本,后续当前线程对共享变量的操作,都基于ThreadLcoalMap来进行,不会影响全局共享变量的值
    面试总结之并发编程_第3张图片

2、ThreadLocal在项目中的实际应用

  • 在典型的MVC系统架构中,登录后的用户每次访问接口,都会在请求头中携带一个Token,在控制层可以根据该Token解析出登录用户的基本信息,那如果要在服务层和持久层都要用到登录用户的信息,如RPC调用,更新用户信息等,那要该如何实现?
  • 这时就可以使用ThreadLcoal,在控制层拦截请求,将用户信息存储到ThreadLocal,如此,就可以在服务层和持久层获取到ThreadLcoal中存储的登录用户信息
    面试总结之并发编程_第4张图片
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();
    }
}
  • 使用:
    // 获取当前的登录用户id
    Long userId = UserHolder.getUser().getId();

扩展:

  • 其他场景的cookie、session等数据隔离的操作都可以通过ThreadLcoal实现
  • 数据库连接池中的connection连接交给ThreadLocal来管理,保证当前线程操作的都是同一个connection

3、ThreadLocal实现原理

  • 每个线程都有一个成员变量ThreadLocalMap,当线程访问ThreadLocal修饰的共享数据时,该线程就会在自己的ThreadLocalMap中存储一份共享数据的副本,key指向ThreadLocal这个弱引用,value保存的是共享数据的副本,因为每个线程都有一份共享数据的副本,以此就解决了线程安全问题
    面试总结之并发编程_第5张图片

  • ThreadLocal的set方法:

    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        // 将当前元素存入ThreadLocalMap 
        if (map != null)
            map.set(this, value);
        else
            createMap(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本质上是一个个键值对形式的节点组成的数组,那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
    面试总结之并发编程_第6张图片

小结:
实现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对象的具体实例
    面试总结之并发编程_第7张图片
  • 当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 threadLocal = new ThreadLocal<>();
        // 向主线程中的threadLocal设值
        threadLocal.set("世界上最好的编程语言");

        // 子线程
        Thread sonThread = new Thread(){
            @Override
            public 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.
     * 

* 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
    面试总结之并发编程_第8张图片

  • synchronized修饰代码块时,采用ACC_SYNCHRONIZED来标识该方法是一个synchronized修饰的同步方法
    面试总结之并发编程_第9张图片

  • 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)叫醒等待就诊的线程
    面试总结之并发编程_第10张图片
    小结:
  • monitorenter:在判断拥有同步表示ACC_SYNCHRONIZED后,抢先进入该同步方法的线程会优先拥有Monitor的owner,此时计数器+1
  • monitorexit:当执行完退出后,计数器-1,计数器归零后被其他进入的线程获取
  • 基于Monitor中的计数器,Monitor可以记录锁重入的次数(线程获取锁的次数)

未完待续…

你可能感兴趣的:(Java后端开发,面试总结,Java并发编程,面试,并发编程,后端,Java)