关注「Java视界」公众号,获取更多技术干货

多线程编程

目录

一、进程与线程

二、线程运行状态

三、多线程的代码实现

(1)方式一:继承Thread类

(2)方式二:实现Runnable接口

(3)Callable实现多线程

四、Thread、Runnable、Callable区别与联系

五、多线程常见方法

(1)线程命名与获取

(2)线程的休眠

(3)线程的强制执行

(3)线程的礼让

(4)线程的优先级

六、线程停止

七、volatile关键字

八、守护线程

8.1 怎么创建守护线程?

8.2 怎么通过守护线程来安全地停止其他线程?

九、ThreadLocal

ThreadLocal原理

9.1 源码分析

9.1.1 ThreadLocalMap 和 ThreadLocal 的set()方法

9.1.2 ThreadLocalMap 和 ThreadLocal 的get()方法

9.2 ThreadLocal特性

9.3 ThreadLocal使用场景

接下来看两个具体的例子:

实践场景1

对ThreadLocal的总结

使用ThreadLocal的好处

十、 interrupt()

十一、park()

补充一、join、yield和sleep的区别?

补充二:为什么不推荐使用stop和destrory方法来结束线程的运行?

补充三:线程dump

补充四:如果想共享线程的ThreadLocal数据怎么办?

补充五:ThreadLocal 有什么问题或缺陷?

ThreadLocal是怎么内存泄漏的?

补充六:调用 start()方法时会执行 run()方法,为什么不能直接调用 run()方法?

补充七:怎么检测一个线程是否拥有锁?


Java是支持多线程的编程语言,多线程是相对于单线程(单进程)而言的,传统的DOS系统是单进程的,同一时间段只允许一个进程执行,当出现病毒那么将导致整个系统瘫痪。多线程则允许同一个时间段多个程序轮流运行,轮流抢占CPU资源。

一、进程与线程

线程是在进程的基础上划分的更小的程序单元,线程是在进程的基础上创建并使用的,所以线程依赖进程的支持,但是线程的启动速度要比进程快许多,当时用多线程进行并发处理的时候,执行性能高于进程。

区别:

  1. 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元;
  2. 同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程;
  3. 进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束;
  4. 线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的;
  5. 线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源;
  6. 线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志。

进程与线程的选择取决以下几点:

  1. 需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的;
  2. 线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应;
  3. 因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
  4. 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
  5. 需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

二、线程运行状态

 

  1. 任何的线程对象都是用Thread类进行封装的,且start()后进入就绪状态,但未执行;
  2. 就绪状态的线程当CPU资源调度到它时进入r运行状态;
  3. 进入执行状态的线程对象可能不会一直执行到结束,例如当它让出资源后会进入阻塞状态,阻塞解除后重新进入就绪状态等待CUP调度;
  4. run()方法执行完毕则进入终止状态。

主方法(main()方法)也是一个线程,实际开发中,主线程可以创建若干子线程,让子线程去处理复杂或者耗时的业务

三、多线程的代码实现

实现多线程有3种方式:

  • (1)方式一:继承Thread类

Java提供了一个java.lang.Thread的程序类,继承了这个类的子类并覆写run()方法才能实现多线程处理,如下:

public class ThreadTest extends Thread{
    private String name;
    public ThreadTest(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for (int x = 0; x < 10; x++){
            System.out.println(this.name + "-"+ x);
        }
    }
}

class Test{
    public static void main(String[] args) {
        new ThreadTest("线程1").start();
        new ThreadTest("线程2").start();
        new ThreadTest("线程3").start();
    }
}
线程1-0
线程1-1
线程1-2
线程1-3
线程2-0
线程2-1
线程2-2
线程2-3
线程2-4
线程3-0
线程2-5
线程2-6
线程2-7
线程2-8
线程2-9
线程1-4
线程1-5
线程1-6
线程1-7
线程1-8
线程1-9
线程3-1
线程3-2
线程3-3
线程3-4
线程3-5
线程3-6
线程3-7
线程3-8
线程3-9

可以看出,执行并非按照顺序执行,而是交替执行,说明已经进行了多线程的处理。需要注意的是,启动线程不是直接使用run()方法,而是通过start()方法完成的,且需要注意的是每一个线程对象只允许启动一次

  • (2)方式二:实现Runnable接口

class DemoThread implements Runnable{
    private String content;
    public DemoThread(String content) {
        this.content = content;
    }
    public DemoThread() {
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-"+ i);
        }
    }
}

public class RunnableTest {
    public static void main(String[] args) {
        DemoThread demoThread1 = new DemoThread("线程1");
        DemoThread demoThread2 = new DemoThread();
        Thread t1 = new Thread(demoThread1);
        Thread t2 = new Thread(demoThread2, "线程2");
        t1.start();
        t2.start();
    }
}
线程2-0
Thread-0-0
线程2-1
Thread-0-1
Thread-0-2
Thread-0-3
线程2-2
Thread-0-4
线程2-3
Thread-0-5
线程2-4
Thread-0-6
线程2-5
Thread-0-7
线程2-6
线程2-7
线程2-8
线程2-9
Thread-0-8
Thread-0-9

Runnable这种方式,因为不用继承Thread类,不再有单继承的局限。因此,推荐使用Runnable这种方式实现多线程。

此外,JDK1.8开始,Runnable这种方式还可以利用Lambda表达式进行多线程的实现

public class LambdaTest {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            String name = "线程-" + i;
            Runnable run = ()->{
                for (int j = 0; j < 10; j++) {
                    System.out.println(name + " " + j);
                }
            };
            new Thread(run).start();
        }
    }
}
线程-0 0
线程-1 0
线程-1 1
线程-1 2
线程-1 3
线程-2 0
线程-1 4
线程-2 1
线程-1 5
线程-2 2
线程-1 6
线程-2 3
线程-1 7
线程-2 4
线程-0 1
线程-0 2
线程-0 3
线程-0 4
线程-0 5
线程-0 6
线程-1 8
线程-0 7
线程-2 5
线程-0 8
线程-1 9
线程-0 9
线程-2 6
线程-2 7
线程-2 8
线程-2 9

  • (3)Callable实现多线程

class DThread implements Callable{

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        System.out.println(sum);
        return sum;
    }
}

public class CallableTest {
    public static void main(String[] args) throws Exception{
        FutureTask task = new FutureTask(new DThread());
        new Thread(task).start();
        System.out.println(task.get());
    }
}
705082704
705082704

可以看出,Callable这种方式可以有返回值,这是Runnable方式所不具备的。且Callable还支持泛型。

四、Thread、Runnable、Callable区别与联系

(1)Thread和Runnable的关系

  • Runnable可以避免单继承局限,更好的进行功能扩充;
  • Thread类是Runnable接口的子类,继承Thread类实现的run()方法其实是Runnable的run()方法

(2)Runnable与Callable的区别

  • Runnable出现的较早(JDK1.0),Callable出现较晚(JDK1.5)
  • Runnable提供run()方法,没有返回值;
  • Callable提供call(),有返回值。

五、多线程常见方法

(1)线程命名与获取

  • 构造方法命名:Thread(Runnable target, String name)
  • 设置命名:setName(String name)
  • 获取线程名:getName(String name)
  • 获取当前线程名称:currentThread()

(2)线程的休眠

如果希望一个线程暂缓执行就需要使用休眠处理,通过sleep()方法实现。休眠的主要特点就是可以自动实现线程的唤醒,继续后续处理。

public class LambdaTest {
    public static void main(String[] args) {
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

睡眠过程中可能会报InteruptedException异常,因此sleep()方法必须强制处理或抛出异常。

除了sleep()方法,还有一个wait()方法,wait()方法也可以中断线程的运行,使本线程等待,暂时让出CPU的使用权,并允许其他线程使用这个同步方法

sleep和wait的区别:

① 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。

sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

② 锁: sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中,使得其他线程可以使用同步控制块或者方法。
 

sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。

③ 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

(3)线程的强制执行

正常情况下,线程之间是抢占资源轮流执行的,但当满足某些条件后,某个线程对象可以一直独占资源进行执行,这就是线程的强制执行。强制执行需要调用join()方法。不加join时:

class ThreadDemo extends Thread {
    private String name;
    public ThreadDemo(String name){
        this.name=name;
    }
    @Override
    public void run(){
        for(int i=1; i<=6; i++){
            System.out.println(name+"-"+i);
        }
    }
}

public class JoinTest {
    public static void main(String[] args) {
        ThreadDemo t1 = new ThreadDemo("A");
        ThreadDemo t2 = new ThreadDemo("B");
        t1.start();
        t2.start();
    }
}
A-1
B-1
B-2
B-3
B-4
B-5
A-2
A-3
A-4
A-5

加了join后:

class ThreadDemo extends Thread {
    private String name;
    public ThreadDemo(String name){
        this.name=name;
    }
    @Override
    public void run(){
        for(int i=1; i<=6; i++){
            System.out.println(name+"-"+i);
        }
    }
}

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo t1 = new ThreadDemo("A");
        ThreadDemo t2 = new ThreadDemo("B");
        t1.start();
        t1.join();
        t2.start();
    }
}
A-1
A-2
A-3
A-4
A-5
A-6
B-1
B-2
B-3
B-4
B-5
B-6

可以看出,线程A会在调用join()后强制优先执行,完成后线程B才执行。

join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

(3)线程的礼让

是指先将资源让出去让别的线程先执行。需要调用yield()方法。

class ThreadDemo extends Thread {
    private String name;
    public ThreadDemo(String name){
        this.name=name;
    }
    @Override
    public void run(){
        for(int i=1; i<=6; i++){
            if(i % 2 == 0){
                Thread.yield();
            }
            System.out.println(name+"-"+i);
        }
    }
}

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo t1 = new ThreadDemo("A");
        ThreadDemo t2 = new ThreadDemo("B");
        t1.start();
        t2.start();
    }
}
A-1
B-1
A-2
B-2
B-3
A-3
B-4
A-4
B-5
A-5
B-6
A-6

在能被2整除的位置,线程间会产生礼让,但是这种礼让不绝对,即使A线程已经礼让了,但CUP还是可能在下个执行片段调度到线程A。

(4)线程的优先级

线程的优先级表明线程抢占到资源的概率,优先级高则抢占到资源的概率越大。

有三种优先级,线程的优先级是1-10之间的正整数,线程优先级最高为10,最低为1,默认为5:

  • 1- MIN_PRIORITY
  • 10-MAX_PRIORITY
  • 5-NORM_PRIORITY

六、线程停止

停止某个线程有很多方法:

  • stop()
  • destroy()
  • suspend()

但以上3个方法可能会造成死锁,所以不建议直接使用。

要想停止某个线程,需要更加柔和的方式:

public class Demo2 {
    public static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            long num = 0;
            while(flag){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " " + num++);
            }
        }, "非主线程").start();
        Thread.sleep(200);
        flag = false;
    }
}
非主线程 0
非主线程 1
非主线程 2
非主线程 3
非主线程 4
非主线程 5
非主线程 6
非主线程 7
非主线程 8
非主线程 9

这种停止方式使得线程不会直接立即停下,而是慢慢的停下,更加柔和安全。

七、volatile关键字

volatile关键字的作用:当操作该volatile变量时,所有前序对该变量的操作都已完成(如不存在已变更,但未写回主存的情况),所有后续对该变量的操作,都未开始(保证变量的可见性)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。更抽象一点的说法是volatile关键字可以消除一些非原子操作带来的问题。

在很多线程安全的容器中有应用,在单例双检锁设计模式中也有应用,后面的文章会介绍。

八、守护线程

线程分为两种,用户线程守护线程。 

其实守护线程和用户线程区别不大,可以理解为特殊的用户线程。特殊就特殊在如果程序中所有的用户线程都退出了,那么所有的守护线程就都会被杀死,很好理解,没有被守护的对象了,也不需要守护线程了。

意义及应用场景:当主线程结束时,结束其余的子线程,就免去了还要继续关闭子线程的麻烦。如:Java垃圾回收线程就是一个典型的守护线程。

Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

如果有一个线程没有退出,JVM进程就不会退出。如果某个线程不结束,JVM进程就无法结束。由谁负责结束这个线程然后让JVM退出呢?答案是使用守护线程(Daemon Thread),它可以负责结束线程。

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

8.1 怎么创建守护线程?

创建方式和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

8.2 怎么通过守护线程来安全地停止其他线程?

public class DaemonTest1 {

    public static boolean flag = true;

    static class MyThread implements Runnable {
        @Override
        public void run() {
            while (flag) {
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() +" " + (i+1));
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread(), "用户线程");
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false;
        }, "守护线程!");
        t2.setDaemon(true);

        t1.start();
        t2.start();
    }
}
用户线程 1
用户线程 2
用户线程 3
用户线程 4
用户线程 5
用户线程 1
用户线程 2
用户线程 3
用户线程 4
用户线程 5

flag为true时,用户线程每10ms执行一次,守护线程100ms后将flag置为false,用户线程停止。

九、ThreadLocal

在Thread类中有个threadLocals变量,这是一个ThreadLocal.ThreadLocalMap类型的变量,也就是说ThreadLocalMap是ThreadLocal的静态内部类。从名字可以看出ThreadLocalMap同样是一个Map,Map的key是ThreadLocal对象,这里的ThreadLocal对象是一个弱引用对象,也就是说每当发生GC,ThreadLocal对象就会被回收。

总结一下上面说的内容,Thread类维护着一个ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的静态内部类并且ThreadLocalMap中的Entry的key是ThreadLocal对象的弱引用,value是要存储的内容

这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。各个线程中的ThreadLocal.ThreadLocalMap以及ThreadLocal.ThreadLocal中的值都是不同的对象。

ThreadLocal原理

用个ThreadLocal,我教你一次解决老大难问题

  • Thread
  • ThreadLocal
  • ThreadLocalMap

在Thread类内部有有ThreadLocal.ThreadLocalMap threadLocals = null;这个变量,它用于存储ThreadLocal,因为在同一个线程当中可以有多个ThreadLocal,并且多次调用get()所以需要在内部维护一个ThreadLocalMap用来存储多个ThreadLocal。

使用起来很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值:

public class ThreadLocalTest extends Thread {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>(); 
    private static AtomicInteger ai = new AtomicInteger();

    public ThreadLocalTest(String name) {
        super(name);
    }

    public void run() {
        for (int i = 0; i < 3; i++) {
            // addAndGet()以原子方式将给定值与当前值相加
            threadLocal.set(ai.addAndGet(1) + "");
            System.out.println(this.getName() + " get value-->" + threadLocal.get());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception{
        ThreadLocalTest a = new ThreadLocalTest("ThreadA");
        ThreadLocalTest b = new ThreadLocalTest("ThreadB");
        ThreadLocalTest c = new ThreadLocalTest("ThreadC");
        a.start();
        b.start();
        c.start();
    }
}
ThreadC get value-->3
ThreadB get value-->1
ThreadA get value-->2
ThreadC get value-->4
ThreadB get value-->5
ThreadA get value-->6
ThreadB get value-->8
ThreadC get value-->7
ThreadA get value-->9

从结果我们可以看到,每一个线程都有各自的local值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的local值。

但是它是能做到线程间数据隔离的,所以别的线程使用get()方法是没办法拿到其他线程的值的。

9.1 源码分析

9.1.1 ThreadLocalMap 和 ThreadLocal 的set()方法

ThreadLocalMap是Thread类的一个静态属性,也是ThreadLocal类的一个静态内部类:

上面的代码就可以理解为什么每个线程都可以有一个 ThreadLocalMap 这样的存储结构了。另外,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离

ThreadLocalMap是Thread类的静态内部类,且有一个Entry静态内部类,这个Entry的构造方法可以看出是以ThreadLocal为key的,就是ThreadLocal对象的set(Object obj)方法真正保存值的地方。

继续看ThreadLocal对象的set(Object obj)方法的源码:

ThreadLocal的set方法主要逻辑是:根据当前线程获取当前线程的ThreadLocalMap对象,若这个ThreadLocalMap对象已经存在就以当前ThreadLocal对象为key,要保存的对象为vlue进行保存;若这个ThreadLocalMap对象不存在,就新建一个ThreadLocalMap对象进行保存。再看下ThreadLocalMap的set()方法源码:

可以看出ThreadLocalMap底层是一个Entry[ ]数组,依据key(即ThreadLocal对象)的hashcode相关计算来确定下标,可以看出若是同一个ThreadLocal对象,调用set()方法,那么前一个set的value值会被后一个value值取代。

9.1.2 ThreadLocalMap 和 ThreadLocal 的get()方法

看ThreadLocal对象的get()方法的源码:

可以看出也是取得当前线程,然后根据当前线程获得它的ThreadLocalMap对象,若找到了当前线程的ThreadLocalMap对象,再根据ThreadLocal对象为key获取之前set绑定在该线程上的值。

9.2 ThreadLocal特性

通过上面的源码分析,下面进行特性总结就更容易理解了。

1、ThreadLocal不是集合,它不存储任何内容,真正存储数据的集合在ThreadLocalMap中。ThreadLocal只是一个工具,一个往各个线程的ThreadLocal.ThreadLocalMap中table的某一位置set一个值的工具而已。

2、ThreadLocalMap是线程独有的,别的线程访问不了也没必要访问。

3、ThreadLocal肯定是全局共享的,全局也可以多个ThreadLocal对象。

4、对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。

5、对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

6、对于同一个ThreadLocal对象,set后,table中绝不会多出一个数据,而是后值覆盖前值(这个类比Map就好了,要是key值一样,两次put,之前保存的value值会被覆盖)。

7、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收。假如ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

ThreadLocal和Synchronized的区别:

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是:Synchronized是通过线程等待,牺牲时间来解决访问冲突;ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

Synchronized让每个线程轮流去执行某个对象,ThreadLocal是让每个线程都保留属于线程自己数据副本。

9.3 ThreadLocal使用场景

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离

3、进行事务操作,用于存储线程事务信息。

4、数据库连接,Session会话管理。

因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

比如:利用ThreadLocal保存登录Session信息

一、利用ThreadLocal实现中间类来保存登录信息

public class SessionCache {
	private static ThreadLocal<Session> threadLocal = new ThreadLocal<>();
 
	public static <T extends Session> void put(T t) {
		threadLocal.set(t);
	}
 
	@SuppressWarnings("unchecked")
	public static <T> T get() {
		return (T) threadLocal.get();
	}
 
	public static void remove() {
		threadLocal.remove();
	}
}

二、在程序登录(token)认证拦截器中实现保存和删除功能

public class LoginAuthInterceptor implements HandlerInterceptor {
 
	private static final Logger logger = LoggerFactory.getLogger(LoginAuthInterceptor.class);
 
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		String token = request.getHeader("token");
 
		// 根据token获取登录信息
		UserSession session = new UserSession();
		session.setAccountId("test");
		session.setName("测试");
		SessionCache.put(session);
		logger.info("请求方法方法前拦截");
		return true;
	}
 
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
		SessionCache.remove();
		logger.info("请求方法方法后处理");
	}
}

三、在需要当前登录信息的方法中使用

@RestController
public class TestController {
	private static final Logger logger = LoggerFactory.getLogger(TestController.class);
 
	@RequestMapping(value = "/test/session")
	public String testSession(TestDto dto) {
		UserSession session = SessionCache.get();
		String userId = session.getAccountId();
		logger.info("当前登录用户ID为{}", userId);
		return "success:" + userId;
	}
}

ThreadLocal(线程本地变量)通常理解为“采用了空间换时间的设计思想,主要用来实现在多线程环境下的线程安全和保存线程上下文中的变量”。

还有哪些应用场景?

  • Spring采用Threadlocal,来保证单个线程中数据库操作使用的是同一个数据库连接。

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class); 
private static final ThreadLocal<Map<Object, Object>> resources =   new NamedThreadLocal<>("Transactional resources"); 
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =   new NamedThreadLocal<>("Transaction synchronizations"); 
private static final ThreadLocal<String> currentTransactionName =   new NamedThreadLocal<>("Current transaction name"); 
  • SimpleDataFormat 线程安全问题

用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

接下来看两个具体的例子:

 场景1

每个线程需要一个独享对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

每个Thread内有自己的实例副本,不共享

比喻:教材只有一本,一起做笔记有线程安全问题。复印后没有问题,使用ThradLocal相当于复印了教材。

场景2

每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

实践场景1

两个线程打印日期:

public class ThreadLocalNormalUsage00 {
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(10);
                System.out.println(date);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(104707);
                System.out.println(date);
            }
        }).start();

    }

    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }
}

 中国位于东八区,所以时间从1970年1月1日的8点开始计算的,线程1打印的就是8点10分的。

现在起1000个线程去打印,每个线程间隔100ms执行:

public class ThreadLocalNormalUsage01 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage01().date(finalI);
                    System.out.println(date);
                }
            }).start();
            //线程启动后,休眠100ms
            Thread.sleep(100);
        }
    }

    public String date(int seconds) {

        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }
}

 没有问题。

现在使用线程池去打印,线程池固定大小10个线程,执行1000次时间打印:

public class ThreadLocalNormalUsage02 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            //提交任务
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }
}

也没有问题。但是使用线程池时就会发现每个线程都有一个自己的SimpleDateFormat对象,没有必要,所以将SimpleDateFormat声明为静态,保证只有一个:

public class ThreadLocalNormalUsage03 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    //只创建一次 SimpleDateFormat 对象,避免不必要的资源消耗
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            //提交任务
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}

 出现了秒数相同的打印结果,这显然是不正确的。

因为多个线程的task指向了同一个SimpleDateFormat对象,SimpleDateFormat是非线程安全的。

解决的办法一方面是加锁:

public String date(int seconds) {
    //参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
    Date date = new Date(1000 * seconds);
    String s;
    synchronized (ThreadLocalNormalUsage04.class) {
        s = dateFormat.format(date);
    }
    return s;
}

但因为添加了synchronized,所以会保证同一时间只有一条线程可以执行,这在高并发场景下肯定不是一个好的选择。

另一个方法就是使用`ThreadLocal`:

public class ThreadLocalNormalUsage05 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            //提交任务
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {

        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT 开始计时
        Date date = new Date(1000 * seconds);
        //获取 SimpleDateFormat 对象
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}
class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new
            ThreadLocal<SimpleDateFormat>(){

        //创建一份 SimpleDateFormat 对象
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

利用 ThreadLocal 给每个线程分配自己的 dateFormat 对象,不但保证了线程安全,还高效的利用了内存。使用了ThreadLocal后不同的线程不会有共享的 SimpleDateFormat 对象,所以也就不会有线程安全问题。(这里使用initialValue方法进行保存

 实践场景2

当前用户信息需要被线程内的所有方法共享。

可以将user作为参数在每个方法中进行传递,但是这样做会产生代码冗余问题,并且可维护性差

对此进行改进的方案是使用一个Map,在第一个方法中存储信息,后续需要使用直接get()即可:

 Map方案缺点:如果在单线程环境下可以保证安全,但是在多线程环境下是不可以的。如果使用加锁和ConcurrentHashMap都会产生性能问题。

那这个时候就可以使用ThreadLocal,实现不同方法间的资源共享。

使用 ThreadLocal 可以避免加锁产生的性能问题,也可以避免层层传递参数来实现业务需求,就可以实现不同线程中存储不同信息的要求。

public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {

    public void process() {
        User user = new User("ok");
        //将User对象存储到 holder 中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用户名: " + user.name);
        new Service3().process();
    }
}

class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名: " + user.name);
    }
}


class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}

对ThreadLocal的总结

  • 让某个需要用到的对象实现线程之间的隔离(每个线程都有自己独立的对象)
  • 可以在任何方法中轻松的获取到该对象
  • 根据共享对象生成的时机选择使用initialValue方法还是set方法 对象初始化的时机由我们控制的时候使用initialValue 方式 如果对象生成的时机不由我们控制的时候使用 set 方式

使用ThreadLocal的好处

  • 达到线程安全的目的
  • 不需要加锁,执行效率高
  • 更加节省内存,节省开销
  • 免去传参的繁琐,降低代码耦合度

十、 interrupt()

interrupt 打断线程有两种情况,如下:

  1. 如果一个线程在运行中被打断,打断标记会被置为 true 。
  2. 如果是打断因sleep wait join 方法而被阻塞的线程,抛出异常后会将打断标记重置为 false 。

 第一种:

public class Interrupter03 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(true){
                Thread current = Thread.currentThread();
                boolean currentInterrupted = current.isInterrupted();
                Console.log("t1的线程状态:{}", currentInterrupted);
                if(currentInterrupted){
                    Console.log("被打断了,线程任务退出!");
                    break;
                }
            }
        }, "t1");

        t1.start();
        Thread.sleep(1000);
        Console.log("t1 线程被打断前的打断标记{}", t1.isInterrupted());
        Console.log("main 线程开始中断t1 线程.....");
        t1.interrupt();
        Console.log("t1 线程被打断后的打断标记{}", t1.isInterrupted());
    }
}
t1的线程状态:false
t1的线程状态:false
t1 线程被打断前的打断标记false
t1的线程状态:false
t1的线程状态:false
main 线程开始中断t1 线程.....
t1的线程状态:false
t1的线程状态:false
t1的线程状态:false
t1的线程状态:false
t1的线程状态:false
t1 线程被打断后的打断标记true
t1的线程状态:true
被打断了,线程任务退出!

可以看到:

  • t1线程开始的打断状态是false,在主方法调用了t1线程的interrupt()方法后,t1线程的打断状态是true
  • 主方法调用了t1线程的interrupt()方法,将t1的打断状态改为true的时候是有一些时间间隔的

 第二种:

public class Interrupter01 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            Console.log("t1 sleep.....");
            try {
                Thread.sleep(15000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");

        t1.start();
        Thread.sleep(1000);
        Console.log("t1 线程被打断前的打断标记{}", t1.isInterrupted());
        Console.log("main 线程开始中断t1 线程.....");
        t1.interrupt();
        Console.log("t1 线程被打断后的打断标记{}", t1.isInterrupted());
    }
}
t1 sleep.....
t1 线程被打断前的打断标记false
main 线程开始中断t1 线程.....
t1 线程被打断后的打断标记true
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at thread.demo.Interrupter01.lambda$main$0(Interrupter01.java:10)
	at java.lang.Thread.run(Thread.java:748)

这个不对啊,不是“打断因sleep wait join 方法而被阻塞的线程,抛出异常后会将打断标记重置为 false ”吗?为啥这里打断了还是true?

前面说了,打断后还有一些时间间隔之后,打断状态才能完成重置。打断后,让主线程暂停一会再去获取状态,就可以获得最终的打断状态:

public class Interrupter02 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            Console.log("t1 sleep.....");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");

        t1.start();
        Thread.sleep(1000);
        Console.log("t1 线程被打断前的打断标记{}", t1.isInterrupted());
        Console.log("main 线程开始中断t1 线程.....");
        t1.interrupt();
        Thread.sleep(1000);
        Console.log("t1 线程被打断后的打断标记{}", t1.isInterrupted());
    }
}
t1 sleep.....
t1 线程被打断前的打断标记false
main 线程开始中断t1 线程.....
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at thread.demo.Interrupter02.lambda$main$0(Interrupter02.java:12)
	at java.lang.Thread.run(Thread.java:748)
t1 线程被打断后的打断标记false

应用:两阶段终止(Two Phase Termination)

就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。

public class TwoPhaseTermination {
    private Thread monitor;

    public void start(){
        monitor = new Thread(() -> {
           while(true) {
               Thread currentThread = Thread.currentThread();
               boolean currentThreadInterrupted = currentThread.isInterrupted();
               if(currentThreadInterrupted) {
                   Console.log(DateUtil.now() + "料理后事");
                   break;
               }
               try {
                   Thread.sleep(1000);
                   Console.log(DateUtil.now() + "执行监控");
               } catch (InterruptedException e) {
                   e.printStackTrace();
                   // 重新设置打断状态
                   currentThread.interrupt();
               }
           }
        });
        monitor.start();
    }

    public void stop(){
        monitor.interrupt();
    }
}
2021-08-01 15:50:35执行监控
2021-08-01 15:50:36执行监控
2021-08-01 15:50:37执行监控
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at thread.demo.TwoPhaseTermination.lambda$start$0(TwoPhaseTermination.java:19)
	at java.lang.Thread.run(Thread.java:748)
2021-08-01 15:50:37料理后事

isInterrupted() 与 interrupted() 比较

  • 调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真正的停止线程

  • interrupted()测试当前线程是否已经是中断状态,执行后具有清除中断状态flag的功能

  • isInterrupted()测试线程Thread对象是否已经是中断状态,但不清除中断状态flag 

十一、park()

park()是LockSupport的静态方法,调用后会暂停当前线程的任务。

public class ParkDemo {
    public static void main(String[] args) {
        park();
    }

    private static void park(){
        Thread t1 = new Thread(()-> {
            Console.log(DateUtil.now() + "开始调用park,暂停任务");
            LockSupport.park();
            Console.log(DateUtil.now() + "park后");
        }, "t1");

        t1.start();
    }
}

可以看出调用park()方法后,第二个打印语句没有执行,且任务并没有结束,只是暂停了。

这个时候打断这个暂停就可以继续进行任务执行:

public class ParkDemo {
    public static void main(String[] args) {
        park();
    }

    private static void park(){
        Thread t1 = new Thread(()-> {
            Console.log(DateUtil.now() + "开始调用park,暂停任务");
            LockSupport.park();
            Console.log(DateUtil.now() + "park后");
            Console.log(DateUtil.now() + "打断状态" + Thread.currentThread().isInterrupted());
        }, "t1");

        t1.start();
        sleep(1000);
        t1.interrupt();
    }
}
2021-08-01 16:09:31开始调用park,暂停任务
2021-08-01 16:09:32park后
2021-08-01 16:09:32打断状态true

可以看到,主线程在t1暂停后,打断了t1的暂停,之后t1继续执行。

注意的是,若再次调用LockSupport.park()就没有暂停效果了:

public class ParkDemo {
    public static void main(String[] args) {
        park();
    }

    private static void park(){
        Thread t1 = new Thread(()-> {
            Console.log(DateUtil.now() + "开始调用park,暂停任务");
            LockSupport.park();
            Console.log(DateUtil.now() + "park后");
            Console.log(DateUtil.now() + "打断状态" + Thread.currentThread().isInterrupted());

            LockSupport.park();
            Console.log(DateUtil.now() + "第二次park,打断状态:" + Thread.currentThread().isInterrupted());
        }, "t1");

        t1.start();
        sleep(1000);
        t1.interrupt();
    }
}
Connected to the target VM, address: '127.0.0.1:54985', transport: 'socket'
2021-08-01 16:12:55开始调用park,暂停任务
2021-08-01 16:12:56park后
2021-08-01 16:12:56打断状态true
2021-08-01 16:12:56第二次park,打断状态:true

 可以看到第二次调用park()方法并没有使任务暂停,打印任务继续执行了。 

若想让第二次调用park()能生效,任务再次暂停就要在调用前重置打断状态。前面介绍了interrupted() 这个静态方法,在返回打断状态后就会清空打断状态:

public class ParkDemo {
    public static void main(String[] args) {
        park();
    }

    private static void park(){
        Thread t1 = new Thread(()-> {
            Console.log(DateUtil.now() + "开始调用park,暂停任务");
            LockSupport.park();
            Console.log(DateUtil.now() + "park后");
            Console.log(DateUtil.now() + "打断状态" + Thread.interrupted());

            LockSupport.park();
            Console.log(DateUtil.now() + "第二次park,打断状态:" + Thread.currentThread().isInterrupted());
        }, "t1");

        t1.start();
        sleep(1000);
        t1.interrupt();
    }
}

 interrupted() 后,打断状态重置,第二次调用park()方法后可以看到任务再次暂停。

补充一、join、yield和sleep的区别?

1、join():当前线程等待调用此方法的线程执行结束再继续执行。如:在main方法中调用。如:在main方法中调用 t.join() , 那main方法在此时进入阻塞状态,一直等 t 线程执行完,main方法再恢复到就绪状态,准备继续执行。

2、yield():调用该方法的线程暂停一下,让出cpu的调度,回到就绪状态。所以调用该方法的线程很可能进入就绪状态后马上又被执行。

3、sleep():在指定时间内让线程暂停执行,进入阻塞状态。在指定时间到达后进入就绪状态。线程调用sleep()方法时,释放CPU但不释放对象锁(如果持有某个对象的锁的话)

补充二:为什么不推荐使用stop和destrory方法来结束线程的运行?

stop():此方法可以强行中止一个正在运行或挂起的线程。但stop方法不安全,就像强行切断计算机电源,而不是按正常程序关机。可能会产生不可预料的结果。举例来说:

当在一个线程对象上调用stop()方法时,这个线程对象所运行的线程就会立即停止,并抛出特殊的ThreadDeath()异常。这里的“立即”因为太“立即”了,假如一个线程正在执行:synchronized void{x =3;y=4;}由于方法是同步的,多个线程访问时总能保证x,y被同时赋值,而如果一个线程正在执行到x = 3;时,被调用了stop()方法,即使在同步块中,它也干脆地stop了,这样就产生了不完整的残废数据。而多线程编程中最最基础的条件要保证数据的完整性,所以请忘记线程的stop方法,以后我们再也不要说“停止线程”了。

destroy():该方法最初用于破坏该线程,但不作任何资源释放。它所保持的任何监视器都会保持锁定状态。不过,该方法决不会被实现。即使要实现,它也极有可能以suspend()方式被死锁。如果目标线程被破坏时保持一个保护关键系统资源的锁,则任何线程在任何时候都无法再次访问该资源。如果另一个线程曾试图锁定该资源,则会出现死锁。

补充三:线程dump

线程dump是非常有用的诊断java应用问题的工具,每一个java虚拟机都有及时生成显示所有线程在某一点状态的线程dump能力。虽然每个java虚拟机线程dump打印输出格式上略微有一些不同,但是线程dump的信息包含线程基本信息、线程的运行状态、标识、调用的堆栈;调用的堆栈包含完整的类名,所执行的方法,如果可能的话还有源代码的行数

通过dump线程查看线程信息告诉我们是DeadLockDemo类的第42行和第31行引起的死锁。

补充四:如果想共享线程的ThreadLocal数据怎么办?

前面我们提到过:ThreadLocal可以实现线程间数据隔离,别的线程使用get()方法是没办法拿到其他线程的值的。

但是如果想共享线程的ThreadLocal数据怎么办?

可以使用 InheritableThreadLocal 实现多个线程访问ThreadLocal的值:

    public static void main(String[] args) {
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        threadLocal.set("我是主线程set到threadLocal中的值。");
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                Console.log( "【我是非主线程,从主线程中的threadLocal中取值】:" + threadLocal.get());
            }
        };
        t.start();
    }
【我是非主线程,从主线程中的threadLocal中取值】:我是主线程set到threadLocal中的值。

因此,InheritableThreadLocal 可以解决父子线程数据传递的问题。 

补充五:ThreadLocal 有什么问题或缺陷?

ThreadLocal是怎么内存泄漏的?

由于ThreadLocalMap中的key是ThreadLocal的弱引用,一旦发生GC便会回收ThreadLocal,那么此时的ThreadLocalMap存储的key便是null。如果不通过手动remove()那么ThreadLocalMap的Entry便伴随线程的整个生命周期造成内存泄漏,大致就是一个thread ref -> thread -> threadLocals -> entry -> value的强引用关系。因此Java其实是有对于内存泄漏的一些预防机制的,每次调用ThreadLocal的set()get()remove()方法时都会回收key为空的Entry的value。

调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal后,要调用remove()方法。

class Service1 {
    public void process() {
        User user = new User("鲁毅");
        //将User对象存储到 holder 中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}
class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用户名: " + user.name);
        new Service3().process();
    }
}
class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名: " + user.name);
        //手动释放内存,从而避免内存泄漏
        UserContextHolder.holder.remove();
    }
}

一般,在代码的最后使用remove把值清空就好了:

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("张三");
    ……
} finally {
    localName.remove();
}

remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。

那么为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期,如果key是弱引用,被GC后至少ThreadLocal被回收了,在下一次的set()get()remove()还会回收key为null的Entry的value。

补充六:调用 start()方法时会执行 run()方法,为什么不能直接调用 run()方法?

当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。

但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,

只会把 run 方法当作普通方法去执行。

补充七:怎么检测一个线程是否拥有锁?

在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true ,如果当且仅当当前线程拥有某个具体对象的锁。

posted @ 2022-06-25 14:03  沙滩de流沙  阅读(91)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货