菜鸟笔记
提升您的技术认知

多线程50问!-ag真人游戏

1、为什么要使用多线程

选择多线程的原因,就是因为快。举个例子:

如果要把1000块砖搬到楼顶,假设到楼顶有几个电梯,你觉得用一个电梯搬运快,还是同时用几个电梯同时搬运快呢?这个电梯就可以理解为线程。

所以,我们使用多线程就是因为: 在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用cpu和i/o的利用率,提升程序运行速率。

当然,有利就有弊,多线程场景下,我们要保证线程安全,就需要考虑加锁。加锁如果不恰当,就很很耗性能。

2. 创建线程有几种方式?

java中创建线程主要有以下这几种方式:

  • 定义thread类的子类,并重写该类的run方法

  • 定义runnable接口的实现类,并重写该接口的run()方法

  • 定义callable接口的实现类,并重写该接口的call()方法,一般配合future使用

  • 线程池的方式

2.1 定义thread类的子类,并重写该类的run方法

public class threadtest {
    public static void main(string[] args) {
        thread thread = new mythread();
        thread.start();
    }
}
class mythread extends thread {
    @override
    public void run() {
        system.out.println("关注公众号:捡田螺的小男孩");
    }
}

2.2 定义runnable接口的实现类,并重写该接口的run()方法

public class threadtest {
    public static void main(string[] args) {
        myrunnable myrunnable = new myrunnable();
        thread thread = new thread(myrunnable);
        thread.start();
    }
}
class myrunnable implements runnable {
    @override
    public void run() {
        system.out.println("关注公众号:捡田螺的小男孩");
    }
}
//运行结果:
关注公众号:捡田螺的小男孩

2.3 定义callable接口的实现类,并重写该接口的call()方法

如果想要执行的线程有返回,可以使用callable

public class threadtest {
    public static void main(string[] args) throws executionexception, interruptedexception {
        mythreadcallable mc = new mythreadcallable();
        futuretask ft = new futuretask<>(mc);
        thread thread = new thread(ft);
        thread.start();
        system.out.println(ft.get());
    }
}
class mythreadcallable implements callable {
    @override
    public string call()throws exception {
        return "关注公众号:捡田螺的小男孩";
    }
}
//运行结果:
关注公众号:捡田螺的小男孩

2.4 线程池的方式

日常开发中,我们一般都是用线程池的方式执行异步任务。

public class threadtest {
    public static void main(string[] args) throws exception {
        threadpoolexecutor executorone = new threadpoolexecutor(5, 5, 1,
                timeunit.minutes, new arrayblockingqueue(20), new customizablethreadfactory("tianluo-thread-pool"));
        executorone.execute(() -> {
            system.out.println("关注公众号:捡田螺的小男孩");
        });
        //关闭线程池
        executorone.shutdown();
    }
}

3. start()方法和run()方法的区别

其实startrun的主要区别如下:

  • start方法可以启动一个新线程,run方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程。

  • start方法实现了多线程,而run方法没有实现多线程。

  • start不能被重复调用,而run方法可以。

  • start方法中的run代码可以不执行完,就继续执行下面的代码,也就是说进行了线程切换。然而,如果直接调用run方法,就必须等待其代码全部执行完才能继续执行下面的代码。

大家可以结合代码例子来看看哈~

public class threadtest {
    public static void main(string[] args){
        thread t=new thread(){
            public void run(){
                pong();
            }
        };
        t.start();
        t.run();
        t.run();
        system.out.println("好的,马上去关注:捡田螺的小男孩"  thread.currentthread().getname());
    }
    static void pong(){
        system.out.println("关注公众号:捡田螺的小男孩"  thread.currentthread().getname());
    }
}
//输出
关注公众号:捡田螺的小男孩main
关注公众号:捡田螺的小男孩main
好的,马上去关注:捡田螺的小男孩main
关注公众号:捡田螺的小男孩thread-0

4. 线程和进程的区别

  • 进程是运行中的应用程序,线程是进程的内部的一个执行序列

  • 进程是资源分配的最小单位,线程是cpu调度的最小单位。

  • 一个进程可以有多个线程。线程又叫做轻量级进程,多个线程共享进程的资源

  • 进程间切换代价大,线程间切换代价小

  • 进程拥有资源多,线程拥有资源少地址

  • 进程是存在地址空间的,而线程本身无地址空间,线程的地址空间是包含在进程中的

举个例子:

你打开qq,开了一个进程;打开了迅雷,也开了一个进程。

在qq的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。

所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成qq的运行,那么这“多个工作”分别有一个线程。

所以一个进程管着多个线程。

通俗的讲:“进程是爹妈,管着众多的线程儿子”...

5. 说一下 runnable和 callable有什么区别?

  • runnable接口中的run()方法没有返回值,是void类型,它做的事情只是纯粹地去执行run()方法中的代码而已;

  • callable接口中的call()方法是有返回值的,是一个泛型。它一般配合future、futuretask一起使用,用来获取异步执行的结果。

  • callable接口call()方法允许抛出异常;而runnable接口run()方法不能继续上抛异常;

大家可以看下它俩的api:

 @functionalinterface
public interface callable {
    /**
     * 支持泛型v,有返回值,允许抛出异常
     */
    v call() throws exception;
}
@functionalinterface
public interface runnable {
    /**
     *  没有返回值,不能继续上抛异常
     */
    public abstract void run();
}

为了方便大家理解,写了一个demo,小伙伴们可以看看哈:

/*
 *  @author 关注公众号:捡田螺的小男孩
 *  @date 2022-07-11
 */
public class callablerunnabletest {
    public static void main(string[] args) {
        executorservice executorservice = executors.newfixedthreadpool(5);
        callable callable =new callable() {
            @override
            public string call() throws exception {
                return "你好,callable,关注公众号:捡田螺的小男孩";
            }
        };
        //支持泛型
        future futurecallable = executorservice.submit(callable);
        try {
            system.out.println("获取callable的返回结果:" futurecallable.get());
        } catch (interruptedexception e) {
            e.printstacktrace();
        } catch (executionexception e) {
            e.printstacktrace();
        }
        runnable runnable = new runnable() {
            @override
            public void run() {
                system.out.println("你好呀,runnable,关注公众号:捡田螺的小男孩");
            }
        };
        future futurerunnable = executorservice.submit(runnable);
        try {
            system.out.println("获取runnable的返回结果:" futurerunnable.get());
        } catch (interruptedexception e) {
            e.printstacktrace();
        } catch (executionexception e) {
            e.printstacktrace();
        }
        executorservice.shutdown();
    }
}
//运行结果
获取callable的返回结果:你好,callable,关注公众号:捡田螺的小男孩
你好呀,runnable,关注公众号:捡田螺的小男孩
获取runnable的返回结果:null

6. 聊聊volatile作用,原理

volatile关键字是java虚拟机提供的的最轻量级的同步机制。它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性

我们先来一起回忆下java内存模型(jmm):

  • java虚拟机规范试图定义一种java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台上都能达到一致的内存访问效果。

  • java内存模型规定所有的变量都是存在主内存当中,每个线程都有自己的工作内存。这里的变量包括实例变量和静态变量,但是不包括局部变量,因为局部变量是线程私有的。

  • 线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存。并且每个线程不能访问其他线程的工作内存。

volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性。

volatile保证可见性和禁止指令重排,都跟内存屏障有关。我们来看一段volatile使用的demo代码:

/**
 * 关注公众号:捡田螺的小男孩
 **/
public class singleton {  
   private volatile static singleton instance;  
   private singleton (){}  
   public static singleton getinstance() {  
   if (instance == null) {  
       synchronized (singleton.class) {  
       if (instance == null) {  
           instance = new singleton();  
       }  
       }  
   }  
   return instance;  
   }  
}  

编译后,对比有volatile关键字和没有volatile关键字时所生成的汇编代码,发现有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令,lock指令相当于一个内存屏障

lock指令相当于一个内存屏障,它保证以下这几点:

  1. 重排序时不能把后面的指令重排序到内存屏障之前的位置

  2. 将本处理器的缓存写入内存

  3. 如果是写入动作,会导致其他处理器中对应的缓存无效。

第2点和第3点就是保证volatile保证可见性的体现嘛,第1点就是禁止指令重排的体现

内存屏障四大分类:(load 代表读取指令,store代表写入指令)

  • 在每个volatile写操作的前面插入一个storestore屏障。

  • 在每个volatile写操作的后面插入一个storeload屏障。

  • 在每个volatile读操作的后面插入一个loadload屏障。

  • 在每个volatile读操作的后面插入一个loadstore屏障。

有些小伙伴,可能对这个还是有点疑惑,内存屏障这玩意太抽象了。我们照着代码看下吧:

内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦,同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性,哈哈~有关于volatile的底层实现,我们就讨论到这哈~

7. 说说并发与并行的区别?

并发和并行最开始都是操作系统中的概念,表示的是cpu执行多个任务的方式。

  • 顺序:上一个开始执行的任务完成后,当前任务才能开始执行

  • 并发:无论上一个开始执行的任务是否完成,当前任务都可以开始执行

(即 a b 顺序执行的话,a 一定会比 b 先完成,而并发执行则不一定。)

  • 串行:有一个任务执行单元,从物理上就只能一个任务、一个任务地执行

  • 并行:有多个任务执行单元,从物理上就可以多个任务一起执行

(即在任意时间点上,串行执行时必然只有一个任务在执行,而并行则不一定。)

知乎有个很有意思的回答,大家可以看下:

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。

  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。

  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是同时

来源:知乎

8.synchronized 的实现原理以及锁优化?

synchronized是java中的关键字,是一种同步锁。synchronized关键字可以作用于方法或者代码块。

一般面试时。可以这么回答:

8.1 monitorenter、monitorexit、acc_synchronized

如果synchronized作用于代码块,反编译可以看到两个指令:monitorenter、monitorexitjvm使用monitorenter和monitorexit两个指令实现同步;如果作用synchronized作用于方法,反编译可以看到accsynchronized标记,jvm通过在方法访问标识符(flags)中加入accsynchronized来实现同步功能。

  • 同步代码块是通过monitorenter和monitorexit来实现,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。

  • 同步方法是通过中设置accsynchronized标志来实现,当线程执行有accsynchroni标志的方法,需要获得monitor锁。每个对象都与一个monitor相关联,线程可以占有或者释放monitor。

8.2 monitor监视器

monitor是什么呢?操作系统的管程(monitors)是概念原理,objectmonitor是它的原理实现。

在java虚拟机(hotspot)中,monitor(管程)是由objectmonitor实现的,其主要数据结构如下:

 objectmonitor() {
    _header       = null;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = null;
    _owner        = null;
    _waitset      = null;  // 处于wait状态的线程,会被加入到_waitset
    _waitsetlock  = 0 ;
    _responsible  = null ;
    _succ         = null ;
    _cxq          = null ;
    freenext      = null ;
    _entrylist    = null ;  // 处于等待锁block状态的线程,会被加入到该列表
    _spinfreq     = 0 ;
    _spinclock    = 0 ;
    owneristhread = 0 ;
  }

objectmonitor中几个关键字段的含义如图所示:

8.3 java monitor 的工作机理

  • 想要获取monitor的线程,首先会进入_entrylist队列。

  • 当某个线程获取到对象的monitor后,进入owner区域,设置为当前线程,同时计数器count加1。

  • 如果线程调用了wait()方法,则会进入waitset队列。它会释放monitor锁,即将owner赋值为null,count自减1,进入waitset队列阻塞等待。

  • 如果其他线程调用 notify() / notifyall() ,会唤醒waitset中的某个线程,该线程再次尝试获取monitor锁,成功即进入owner区域。

  • 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。

8.4 对象与monitor关联

  • 在hotspot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(header),实例数据(instance data)和对象填充(padding)

  • 对象头主要包括两部分数据:mark word(标记字段)、class pointer(类型指针)

mark word 是用于存储对象自身的运行时数据,如哈希码(hashcode)、gc分代年龄、锁状态标志、线程持有的锁、偏向线程 id、偏向时间戳等。

重量级锁,指向互斥量的指针。其实synchronized是重量级锁,也就是说synchronized的对象锁,mark word锁标识位为10,其中指针指向的是monitor对象的起始地址。

9. 线程有哪些状态?

线程有6个状态,分别是:new, runnable, blocked, waiting, timed_waiting, terminated

转换关系图如下:

  • new:线程对象创建之后、但还没有调用start()方法,就是这个状态。

/**
 * 关注公众号:捡田螺的小男孩
 */
public class threadtest {
    public static void main(string[] args) {
        thread thread = new thread();
        system.out.println(thread.getstate());
    }
}
//运行结果:
new
  • runnable:它包括就绪(ready)和运行中(running)两种状态。如果调用start方法,线程就会进入runnable状态。它表示我这个线程可以被执行啦(此时相当于ready状态),如果这个线程被调度器分配了cpu时间,那么就可以被执行(此时处于running状态)。

public class threadtest {
    public static void main(string[] args) {
        thread thread = new thread();
        thread.start();
        system.out.println(thread.getstate());
    }
}
//运行结果:
runnable
  • blocked: 阻塞的(被同步锁或者io锁阻塞)。表示线程阻塞于锁,线程阻塞在进入synchronized关键字修饰的方法或代码块(等待获取锁)时的状态。比如前面有一个临界区的代码需要执行,那么线程就需要等待,它就会进入这个状态。它一般是从runnable状态转化过来的。如果线程获取到锁,它将变成runnable状态

thread t = new thread(new runnable {
    void run() {
        synchronized (lock) { // 阻塞于这里,变为blocked状态
            // dothings
        } 
    }
});
t.getstate(); //新建之前,还没开始调用start方法,处于new状态
t.start(); //调用start方法,就会进入runnable状态
  • waiting : 永久等待状态,进入该状态的线程需要等待其他线程做出一些特定动作(比如通知)。处于该状态的线程不会被分配cpu执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。一般object.wait

thread t = new thread(new runnable {
    void run() {
        synchronized (lock) { // blocked
            // dothings
            while (!condition) {
                lock.wait(); // into waiting
            }
        } 
    }
});
t.getstate(); // new
t.start(); // runnable
  • timed_wating: 等待指定的时间重新被唤醒的状态。有一个计时器在里面计算的,最常见就是使用thread.sleep方法触发,触发后,线程就进入了timed_waiting状态,随后会由计时器触发,再进入runnable状态。

thread t = new thread(new runnable {
    void run() {
        thread.sleep(1000); // timed_waiting
    }
});
t.getstate(); // new
t.start(); // runnable
  • 终止(terminated):表示该线程已经执行完成。

再来看个代码demo吧:

/**
 * 关注公众号:捡田螺的小男孩
 */
public class threadtest {
    private static object object = new object();
    public static void main(string[] args) throws exception {
        thread thread = new thread(new runnable() {
            @override
            public void run() {
                try {
                    for(int i = 0; i< 1000; i  ){
                        system.out.print("");
                    }
                    thread.sleep(500);
                    synchronized (object){
                        object.wait();
                    }
                } catch (interruptedexception e) {
                    e.printstacktrace();
                }
            }
        });
        thread thread1 = new thread(new runnable() {
            @override
            public void run() {
                try {
                    synchronized (object){
                        thread.sleep(1000);
                    }
                    thread.sleep(1000);
                    synchronized (object){
                        object.notify();
                    }
                } catch (interruptedexception e) {
                    e.printstacktrace();
                }
            }
        });
        
        system.out.println("1" thread.getstate());
        thread.start();
        thread1.start();
        system.out.println("2" thread.getstate());
        while (thread.isalive()){
            system.out.println("---" thread.getstate());
            thread.sleep(100);
        }
        system.out.println("3" thread.getstate());
    }
}
运行结果:
1new
2runnable
---runnable
---timed_waiting
---timed_waiting
---timed_waiting
---timed_waiting
---blocked
---blocked
---blocked
---blocked
---blocked
---waiting
---waiting
---waiting
---waiting
---waiting
---waiting
---waiting
---waiting
---waiting

10. synchronized和reentrantlock的区别?

  • synchronized是依赖于jvm实现的,而reentrantlockapi实现的。

  • synchronized优化以前,synchronized的性能是比reentrantlock差很多的,但是自从synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者性能就差不多了。

  • synchronized的使用比较方便简洁,它由编译器去保证锁的加锁和释放。而reentrantlock需要手工声明来加锁和释放锁,最好在finally中声明释放锁。

  • reentrantlock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁。

  • reentrantlock可响应中断、可轮回,而synchronized是不可以响应中断的

11. wait(),notify()和suspend(),resume()之间的区别

  • wait()方法使得线程进入阻塞等待状态,并且释放锁

  • notify()唤醒一个处于等待状态的线程,它一般跟wait()方法配套使用。

  • suspend()使得线程进入阻塞状态,并且不会自动恢复,必须对应的resume()被调用,才能使得线程重新进入可执行状态。suspend()方法很容易引起死锁问题。

  • resume()方法跟suspend()方法配套使用。

suspend()不建议使用,因为suspend()方法在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

12. cas?cas 有什么缺陷,如何解决?

cas,全称是compare and swap,翻译过来就是比较并交换;

cas涉及3个操作数,内存地址值v,预期原值a,新值b;如果内存位置的值v与预期原a值相匹配,就更新为新值b,否则不更新

cas有什么缺陷?

  • aba 问题

并发环境下,假设初始条件是a,去修改数据时,发现是a就会执行修改。但是看到的虽然是a,中间可能发生了a变b,b又变回a的情况。此时a已经非彼a,数据即使成功修改,也可能有问题。

可以通过atomicstampedreference 解决aba问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证cas的正确性。

  • 循环时间长开销

自旋cas,如果一直循环执行,一直不成功,会给cpu带来非常大的执行开销。很多时候,cas思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

  • 只能保证一个变量的原子操作。

cas 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,cas 目前无法直接保证操作的原子性的。可以通过这两个方式解决这个问题:1. 使用互斥锁来保证原子性; 2.将多个变量封装成对象,通过atomicreference来保证原子性。

有兴趣的朋友可以看看我之前的这篇实战文章哈~cas乐观锁解决并发问题的一次实践

13. 说说countdownlatch与cyclicbarrier 区别

countdownlatch和cyclicbarrier都用于让线程等待,达到一定条件时再运行。主要区别是:

  • countdownlatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;

  • cyclicbarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

举个例子吧:

  • countdownlatch:假设老师跟同学约定周末在公园门口集合,等人齐了再发门票。那么,发门票(这个主线程),需要等各位同学都到齐(多个其他线程都完成),才能执行。

  • cyclicbarrier:多名短跑运动员要开始田径比赛,只有等所有运动员准备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。

14. 什么是多线程环境下的伪共享

14.1 什么是伪共享?

cpu的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改相互独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享

现代计算机计算模型:

  • cpu执行速度比内存速度快好几个数量级,为了提高执行效率,现代计算机模型演变出cpu、缓存(l1,l2,l3),内存的模型。

  • cpu执行运算时,如先从l1缓存查询数据,找不到再去l2缓存找,依次类推,直到在内存获取到数据。

  • 为了避免频繁从内存获取数据,聪明的科学家设计出缓存行,缓存行大小为64字节。

也正是因为缓存行的存在,就导致了伪共享问题,如图所示:

假设数据a、b被加载到同一个缓存行。

  • 当线程1修改了a的值,这时候cpu1就会通知其他cpu核,当前缓存行(cache line)已经失效。

  • 这时候,如果线程2发起修改b,因为缓存行已经失效了,所以「core2 这时会重新从主内存中读取该 cache line 数据」。读完后,因为它要修改b的值,那么cpu2就通知其他cpu核,当前缓存行(cache line)又已经失效。

  • 酱紫,如果同一个cache line的内容被多个线程读写,就很容易产生相互竞争,频繁回写主内存,会大大降低性能。

14.2 如何解决伪共享问题

既然伪共享是因为相互独立的变量存储到相同的cache line导致的,一个缓存行大小是64字节。那么,我们就可以使用空间换时间的方法,即数据填充的方式,把独立的变量分散到不同的cache line~

来看个例子:

/**
 * 更多干货内容,关注公众号:捡田螺的小男孩
 */
public class falsesharetest  {
    public static void main(string[] args) throws interruptedexception {
        rectangle rectangle = new rectangle();
        long begintime = system.currenttimemillis();
        thread thread1 = new thread(() -> {
            for (int i = 0; i < 100000000; i  ) {
                rectangle.a = rectangle.a   1;
            }
        });
        thread thread2 = new thread(() -> {
            for (int i = 0; i < 100000000; i  ) {
                rectangle.b = rectangle.b   1;
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        system.out.println("执行时间"   (system.currenttimemillis() - begintime));
    }
}
class rectangle {
    volatile long a;
    volatile long b;
}
//运行结果:
执行时间2815

一个long类型是8字节,我们在变量a和b之间不上7个long类型变量呢,输出结果是啥呢?如下:

class rectangle {
    volatile long a;
    long a1,a2,a3,a4,a5,a6,a7;
    volatile long b;
}
//运行结果
执行时间1113

可以发现利用填充数据的方式,让读写的变量分割到不同缓存行,可以很好挺高性能~

15. fork/join框架的理解

fork/join框架是java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

fork/join框架需要理解两个点,「分而治之」和「工作窃取算法」。

分而治之

以上fork/join框架的定义,就是分而治之思想的体现啦

工作窃取算法

把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~

工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。

16. 聊聊threadlocal原理?

threadlocal的内存结构图

为了对threadlocal有个宏观的认识,我们先来看下threadlocal的内存结构图

从内存结构图,我们可以看到:

  • thread类中,有个threadlocal.threadlocalmap 的成员变量。

  • threadlocalmap内部维护了entry数组,每个entry代表一个完整的对象,keythreadlocal本身,valuethreadlocal的泛型对象值。

关键源码分析

对照着关键源码来看,更容易理解一点哈~

首先看下thread类的源码,可以看到成员变量threadlocalmap的初始值是为null

public class thread implements runnable {
   //threadlocal.threadlocalmap是thread的属性
   threadlocal.threadlocalmap threadlocals = null;
}

成员变量threadlocalmap的关键源码如下:

static class threadlocalmap {
    
    static class entry extends weakreference> {
        /** the value associated with this threadlocal. */
        object value;
        entry(threadlocal k, object v) {
            super(k);
            value = v;
        }
    }
    //entry数组
    private entry[] table;
    
    // threadlocalmap的构造器,threadlocal作为key
    threadlocalmap(threadlocal firstkey, object firstvalue) {
        table = new entry[initial_capacity];
        int i = firstkey.threadlocalhashcode & (initial_capacity - 1);
        table[i] = new entry(firstkey, firstvalue);
        size = 1;
        setthreshold(initial_capacity);
    }
}

threadlocal类中的关键set()方法:

 public void set(t value) {
        thread t = thread.currentthread(); //获取当前线程t
        threadlocalmap map = getmap(t);  //根据当前线程获取到threadlocalmap
        if (map != null)  //如果获取的threadlocalmap对象不为空
            map.set(this, value); //k,v设置到threadlocalmap中
        else
            createmap(t, value); //创建一个新的threadlocalmap
    }
    
     threadlocalmap getmap(thread t) {
       return t.threadlocals; //返回thread对象的threadlocalmap属性
    }
    void createmap(thread t, t firstvalue) { //调用threadlocalmap的构造函数
        t.threadlocals = new threadlocalmap(this, firstvalue); this表示当前类threadlocal
    }
    

threadlocal类中的关键get()方法

    public t get() {
        thread t = thread.currentthread();//获取当前线程t
        threadlocalmap map = getmap(t);//根据当前线程获取到threadlocalmap
        if (map != null) { //如果获取的threadlocalmap对象不为空
            //由this(即threadloca对象)得到对应的value,即threadlocal的泛型值
            threadlocalmap.entry e = map.getentry(this);
            if (e != null) {
                @suppresswarnings("unchecked")
                t result = (t)e.value; 
                return result;
            }
        }
        return setinitialvalue(); //初始化threadlocals成员变量的值
    }
    
     private t setinitialvalue() {
        t value = initialvalue(); //初始化value的值
        thread t = thread.currentthread(); 
        threadlocalmap map = getmap(t); //以当前线程为key,获取threadlocals成员变量,它是一个threadlocalmap
        if (map != null)
            map.set(this, value);  //k,v设置到threadlocalmap中
        else
            createmap(t, value); //实例化threadlocals成员变量
        return value;
    }

所以怎么回答threadlocal的实现原理?如下,最好是能结合以上结构图一起说明哈~

  • thread线程类有一个类型为threadlocal.threadlocalmap的实例变量threadlocals,即每个线程都有一个属于自己的threadlocalmap

  • threadlocalmap内部维护着entry数组,每个entry代表一个完整的对象,keythreadlocal本身,valuethreadlocal的泛型值。

  • 并发多线程场景下,每个线程thread,在往threadlocal里设置值的时候,都是往自己的threadlocalmap里存,读也是以某个threadlocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离

大家可以看下我之前这篇文章哈: threadlocal的八个关键知识点

17. treadlocal为什么会导致内存泄漏呢?

  • 弱引用导致的内存泄漏呢?

  • key是弱引用,gc回收会影响threadlocal的正常工作嘛?

  • threadlocal内存泄漏的demo

17.1 弱引用导致的内存泄漏呢?

我们先来看看treadlocal的引用示意图哈:

关于threadlocal内存泄漏,网上比较流行的说法是这样的:

threadlocalmap使用threadlocal弱引用作为key,当threadlocal变量被手动设置为null,即一个threadlocal没有外部强引用来引用它,当系统gc时,threadlocal一定会被回收。这样的话,threadlocalmap中就会出现keynullentry,就没有办法访问这些keynullentryvalue,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些keynullentryvalue就会一直存在一条强引用链:thread变量 -> thread对象 -> threalocalmap -> entry -> value -> object 永远无法回收,造成内存泄漏。

threadlocal变量被手动设置为null后的引用链图:

实际上,threadlocalmap的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在threadlocalget,set,remove方法,都会清除线程threadlocalmap里所有keynullvalue

源代码中,是有体现的,如threadlocalmapset方法:

  private void set(threadlocal key, object value) {
      entry[] tab = table;
      int len = tab.length;
      int i = key.threadlocalhashcode & (len-1);
      for (entry e = tab[i];
            e != null;
            e = tab[i = nextindex(i, len)]) {
          threadlocal k = e.get();
          if (k == key) {
              e.value = value;
              return;
          }
           //如果k等于null,则说明该索引位之前放的key(threadlocal对象)被回收了,这通常是因为外部将threadlocal变量置为null,
           //又因为entry对threadlocal持有的是弱引用,一轮gc过后,对象被回收。
            //这种情况下,既然用户代码都已经将threadlocal置为null,那么也就没打算再通过该对象作为key去取到之前放入threadlocalmap的value, 因此threadlocalmap中会直接替换调这种不新鲜的entry。
          if (k == null) {
              replacestaleentry(key, value, i);
              return;
          }
        }
        tab[i] = new entry(key, value);
        int sz =   size;
        //触发一次log2(n)复杂度的扫描,目的是清除过期entry  
        if (!cleansomeslots(i, sz) && sz >= threshold)
          rehash();
    }

如threadlocal的get方法:

  public t get() {
    thread t = thread.currentthread();
    threadlocalmap map = getmap(t);
    if (map != null) {
        //去threadlocalmap获取entry,方法里面有key==null的清除逻辑
        threadlocalmap.entry e = map.getentry(this);
        if (e != null) {
            @suppresswarnings("unchecked")
            t result = (t)e.value;
            return result;
        }
    }
    return setinitialvalue();
}
private entry getentry(threadlocal key) {
        int i = key.threadlocalhashcode & (table.length - 1);
        entry e = table[i];
        if (e != null && e.get() == key)
             return e;
        else
          //里面有key==null的清除逻辑
          return getentryaftermiss(key, i, e);
    }
        
private entry getentryaftermiss(threadlocal key, int i, entry e) {
        entry[] tab = table;
        int len = tab.length;
        while (e != null) {
            threadlocal k = e.get();
            if (k == key)
                return e;
            // entry的key为null,则表明没有外部引用,且被gc回收,是一个过期entry
            if (k == null)
                expungestaleentry(i); //删除过期的entry
            else
                i = nextindex(i, len);
            e = tab[i];
        }
        return null;
    }

17.2 key是弱引用,gc回收会影响threadlocal的正常工作嘛?

有些小伙伴可能有疑问,threadlocalkey既然是弱引用.会不会gc贸然把key回收掉,进而影响threadlocal的正常使用?

  • 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次gc将会回收掉该对象(不管当前内存空间足够与否)

其实不会的,因为有threadlocal变量引用着它,是不会被gc回收的,除非手动把threadlocal变量设置为null,我们可以跑个demo来验证一下:

  public class weakreferencetest {
    public static void main(string[] args) {
        object object = new object();
        weakreference
网站地图