java核心知识体系之基础总结篇五(并发基础一)

101 阅读40分钟

同步和异步

同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。如图所示: 在这里插入图片描述 图中显示了同步方法调用和异步方法调用的区别。对于调用者来说,异步调用似乎是一瞬间就完成的。如果异步调用需要返回结果,那么当这个异步调用真实完成时,则会通知调用者。

举个例子,假如你要去商城买个东西,当你到达商城挑选完东西后,你需要去找售货员付款,还要将东西带回家,那么这个过程就是同步调用,但是假如你需要的那个东西在网上有售卖,那你可以在网上进行购买,当你完成付款后,这个网购的过程其实已经是结束了,那么剩下的就是快递小哥将你购买的东西送给你,这就是异步调用。

并发和并行

并发

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。如图所示: 在这里插入图片描述 特点

1、程序与计算不再一一对应,一个程序副本可以有多个计算。 2、并发程序之间有相互制约关系,直接制约体现为一个程序需要另一个程序的计算结果,间接制约体现为多个程序竞争某一资源,如处理机、缓冲等。 3、并发程序在执行中是走走停停,断续推进的。

程序并发的过程,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。

并行

在操作系统中,若干个程序段同时在系统中运行,这些程序的执行在时间上是重叠的,一个程序段的执行尚未结束,另一个程序段的执行已经开始,无论从微观还是宏观,程序都是一起执行的。是顺序执行)。对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。如图所示: 在这里插入图片描述 当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行。

小结

并发说的是在一个时间段内,多件事情在这个时间段内交替执行。并行说的是多件事情在同一个时刻同时发生。实际上,如果系统内只有一个CPU,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的,毕竟一个CPU一次只能执行一条指令,在这种情况下多进程或者多线程就是并发的,而不是并行的(操作系统会不停地切换多任务)。真实的并行也只可能出现在拥有多个CPU的系统中(比如多核CPU)。

临界区

临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用,但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。

比如,一个办公室里有一台打印机,打印机一次只能执行一个任务。如果小王和小明同时需要打印文件,很明显,如果小王先发了打印任务,打印机就开始打印小王的文件,小明的任务就只能等待小王打印结束后才能打印,这里的打印机就是一个临界区的例子。

在并行程序中,临界区资源是保护的对象,如果意外出现打印机同时执行两个任务的情况,那么最有可能的结果就是打印出来的文件是损坏的文件,它既不是小王想要的,也不是小明想要的。

阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞和非阻塞通常用来形容很多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他线程阻塞在这个临界区上的线程都不能工作。

非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行。

死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

死锁、饥饿和活锁都属于多线程的活跃性问题。如果发现上述几种情况,那么相关线程就不再活跃,也就是说它可能很难再继续往下执行了。

死锁

两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。 例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。 例如:

/**
 * @author: 随风飘的云
 * @describe:死锁例子
 * @date 2022/09/10 0:57
 */
public class DeadLockDemo {
    private static String A = "A";
    private static String B = "B";
    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }
    private void deadLock() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A) {
                    try {
                        Thread.currentThread().sleep(2000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }synchronized (B) {
                        System.out.println("1");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B) {
                    synchronized (A) {
                        System.out.println("2");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

使用jstack查看线程执行可得如下图:(使用方法,首先运行死锁程序,然后再cmd下输入jps获取死锁程序的pid,接着使用jstack pid可查看程序的运行) 在这里插入图片描述

饥饿

饥饿是指某一个或者多个线程因为种种原因无法获得所要的资源,导致一直无法执行。比如它的优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。在自然界中,母鸟给雏鸟喂食很容易出现这种情况:由于雏鸟很多,食物有限,雏鸟之间的事务竞争可能非常厉害,经常抢不到事务的雏鸟有可能被饿死。线程的饥饿非常类似这种情况。此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。于死锁想必,饥饿还是有可能在未来一段时间内解决的(比如,高优先级的线程已经完成任务,不再疯狂执行)。 实例:

import java.util.concurrent.*;

public class ExecutorLock {
    private static ExecutorService single = Executors.newSingleThreadExecutor();

    public static class AnotherCallable implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println("随风飘的云");
            return "annother success";
        }
    }

    public static class MyCallable implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println("随风飘的云");
            Future<String> submit = single.submit(new AnotherCallable());
            return "success" + submit.get();
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable callable = new MyCallable();
        Future<String> future = single.submit(callable);
        System.out.println(future.get());
        System.out.println("over");
        single.shutdown();
    }
}

结果: 在这里插入图片描述 使用jstack查询可得:

"pool-1-thread-1" #12 prio=5 os_prio=0 tid=0x000001e144b0a000 nid=0x53ec waiting on condition [0x00000069768ff000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076bc18780> (a java.util.concurrent.FutureTask)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
        at java.util.concurrent.FutureTask.get(FutureTask.java:191)
        at ThreadTest.ExecutorLock$MyCallable.call(ExecutorLock.java:26)
        at ThreadTest.ExecutorLock$MyCallable.call(ExecutorLock.java:21)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
"main" #1 prio=5 os_prio=0 tid=0x000001e127f7a800 nid=0x4c1c waiting on condition [0x00000069753ff000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076b9c1560> (a java.util.concurrent.FutureTask)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
        at java.util.concurrent.FutureTask.get(FutureTask.java:191)
        at ThreadTest.ExecutorLock.main(ExecutorLock.java:33)

代码分析: 堆栈信息结合图中的代码,可以看出主线程在29行处于等待中,线程池中的工作线程在22行处于等待中,等待获取结果。由于线程池是一个线程,AnotherCallable得不到执行,而被饿死,最终导致了程序死锁的现象。在这里插入图片描述

上下文切换

单核的处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现多线程执行的机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

比如说我们正在读一本书,但是这个时候你的妈妈叫你吃饭了,那你得放下手中的书去吃饭是不是(我试过没听我妈的话,被骂了好久,莫尝试!)。那这个时候你得记住你看的书看到了第几页第几行,当你吃完饭回来再接着看书。但这个时候你再看书,是不是会因为刚刚吃完饭了,所以会对你读书的专心程度有影响。这就是上下文切换,同样,上下文切换会影响多线程的执行速度。

实例代码测试:

public class ConcurrencyTest {

    private static final long count = 100000001;

    // 并发执行
    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (int i = 0; i < count; i++) {
                    a+=5;
                }
            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        thread.join();
        System.out.println("concurrency : "+time+"ms,b = "+b);
    }

    // 串行执行
    private static void serial(){
        long start = System.currentTimeMillis();
        int a = 0;
        for (int i = 0; i < count; i++) {
            a+=5;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("concurrency : "+time+"ms,b = "+b+",a = "+a);
    }
    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
    }
}

在这里插入图片描述 这是串行和并行的执行数据,不同的电脑不同的配置,所得到的数据可能不一样。当数据量比较小的时候为什么串行的代码比并发执行的代码要快?因为并发执行的java程序在运行的过程中有线程的创建和上下文切换的开销。不过可以得出这样的一个结论:并发执行的代码不一定比串行执行的代码快。

减少上下文切换的方法

1、无锁并发编程 : 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。 2、CAS算法: Java的Atomic包使用CAS算法来更新数据,而不需要加锁。 3、使用最少线程: 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。 4、使用协程: 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

死锁

死锁代码演示

public class DeadLockDemo {

    private static String A = "a";
    private static String B = "b";

    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }
    private void deadLock(){
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A){
                    try{
                        Thread.sleep(2000);
                    } catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    synchronized (B){
                        System.out.println("123");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B){
                   synchronized (A){
                       System.out.println("234");
                   }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

资源限制的挑战

什么是资源限制?

资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。

举例说明:例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等。

资源限制引起的问题

在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。例如,一个java程序需要调用CPU和存储器,网络设备,一句话说吧:利用多线程实现同时下载多个目标,然后计算机受限于网络速度,计算机存储速度,计算机CPU的处理速度,这个时候往往需要好几个小时都无法完成的一个多线程任务,但是单线程的话,一个多小时就可以完成任务了。

如何有效解决资源限制问题

硬件资源: 现在考虑使用集群并行执行程序,因为单机的资源有限制。 软件资源: 考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。

如何在资源限制的情况下,让程序执行得更快呢? 方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。

三大根源性问题

并发编程的三大根源性问题是可见性,原子性和有序性,首先模拟一个并发编程的经典问题,下面程序执行的结果是死循环的,首先执行的是线程1,然后整个线程睡眠1000毫秒(也就是一秒)。然后线程2修改了flag的值,这个时候线程1读取不到flag的值,所以也就没有下文了。 在这里插入图片描述 实例代码:

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/25 22:14
 */
public class ThreadVisibility {

    private int flag = 1;

    public void refreshCount(){
        flag = 0;
        System.out.println(Thread.currentThread().getName() + "线程修改了flag的值,flag: " + flag);
    }

    public void load(){
        System.out.println(Thread.currentThread().getName() + "线程开始执行了");
        int i = 0;
        while (flag == 1){
            i ++;
        }
        System.out.println(Thread.currentThread().getName() + "线程跳出循环 i:" + i);
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadVisibility visibility1 = new ThreadVisibility();
        Thread thread1 = new Thread(() -> visibility1.load(), "线程1");
        thread1.start();
        Thread.sleep(1000);
        Thread thread2 = new Thread(() -> visibility1.refreshCount(), "线程2");
        thread2.start();
    }
}

结果: 在这里插入图片描述

可见性:CPU缓存引起

可见性的意思是当一个线程修改一个共享变量时,另外一个线程能立刻读到这个修改的值。

什么是可见性问题呢? 对于如今的多核处理器,每颗CPU都有自己的缓存,缓存读取的速度非常快,但是缓存仅仅对它所在的处理器可见,CPU缓存与内存的数据不容易保证一致。因此为了避免处理器停顿下来等待向内存写入数据而产生的延迟,处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区合并对同一内存地址的多次写,并以批处理的方式刷新,也就是说写缓冲区不会即时将数据刷新到主内存中。这个时候缓存不能及时刷新向内存中写入数据,这就导致了可见性问题。

实例:

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。 此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10. 这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

原子性: 分时复用引起

什么是原子性操作 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整个操作视作一个整体是原子性的核心特征。在Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。

实例代码:

 		int i = 0; //1
        int j = i; //2

        i ++; // 3

        i = j + 1; // 4

        System.out.println(i);
        System.out.println(j);

这里就有一个有趣的问题了,上面的四个操作,有那几个是原子性操作?在单线程的环境下,可以默认上面的四个操作都是原子性操作,但是在多线程的环境下则不同,在java中,只有基本数据类型的赋值才是原子性操作。

编译后的底层指令集文件:

// 取常量0,保存
 0 iconst_0
 1 istore_1
// 创建变量j 并赋值为i。
 2 iload_1
 3 istore_2
// 执行i ++;
 4 iinc 1 by 1
 7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
// 执行 i = j + 1;
14 iload_2
15 iconst_1
16 iadd
17 istore_1

18 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
21 iload_1
22 invokevirtual #3 <java/io/PrintStream.println : (I)V>
25 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
28 iload_2
29 invokevirtual #3 <java/io/PrintStream.println : (I)V>
32 return

显而易见,操作1是原子性操作,操作2中包含了两个操作:读取 i ,将 i 赋值给 j ,操作3中包含了三个操作: 读取 i 值,将 i + 1, 将 改变后的 i 值赋值给 i,操作四中包含了三个操作,过程跟操作三一样。 再来看下面的例子:

示例代码:

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/26 0:04
 */
public class AtomThread {
    private int a = 0;

    public void add(){
        a ++;
    }

    public static void main(String[] args) {
        AtomThread thread = new AtomThread();
        // 结果输出不到1000
        for (int i = 0; i < 10; i++) {
            new Thread(){
                public void run(){
                    for (int j = 0; j < 100; j++) {
                        thread.add();
                    }
                }
            }.start();
        }
        // 结果输出1000
//        for (int i = 0; i < 10; i++) {
//            for (int j = 0; j < 100; j++) {
//                thread.add();
//            }
//        }
        System.out.println(thread.a);
    }
}

结果: 在这里插入图片描述

代码分析: 上面也提到了什么叫原子性,一个操作步骤可以划分多个,但是每一个分割的操作步骤都必须按照顺序执行,而且原子性就是不可分割的操作。如图所示,当然,程序中有10个线程,图中只是展示了两个线程,基本理论差不多的。当线程1执行到0+1=1这个步骤时,线程2读取了a,那么这个时候线程1没有完全执行完毕a的赋值的增加的过程,所有内存中a的值仍为0,这个时候线程2读取到的值为错误的0,当线程1和线程2执行完毕,这个时候a的值都为1,而不是需要的2,这就产生了错乱了。 在这里插入图片描述

保证原子性 在这里提供一个经典问题:假如某储户从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。 试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。这个时候就体现出了保证原子性的必要性了。

保证原子性的方法: 在这里插入图片描述

有序性:指令重排序引起

即程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题

实例代码:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

代码分析: 上面定义了一个int型的变量 i 和一个boolean 类型的 flag 变量,下面先给变量 i 赋值,然后再给变量 flag赋值,那么就有一个问题了,这些代码在java中执行的顺序是跟代码的先后顺序一致吗?不一定,因为在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。当发生了指令重排序后,这个执行顺序就不一定跟代码的先后顺序一致了。 在这里插入图片描述 上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

重排序类型:

1、编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 2、指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 3、内存系统的重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

进程与线程

1、线程是进程中的一个实体,线程本身是不会独立存在的 。 2、进程是代码在数据集合上的一次运行活动 , 是系统进行资源分配和调度的基本单位 , 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中的多个线程共享进程的资源。 3、系统运行一个程序即是一个进程从创建,运行到消亡的过程。 4、在 Java 中,当我们启动 main 函数时其实就启动了一个 jvm 的进程, 而main 函数所在的线程是这个进程中的一个线程,也称主线程 。

实例代码:

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/22 15:20
 */
public class MultithThreadTest {
    public static void main(String[] args) {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        ThreadInfo[] infos = bean.dumpAllThreads(false, false);
        for (ThreadInfo threadInfo: infos){
            System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
        }
    }
}

结果(不同的电脑可能有不同的输出): 在这里插入图片描述

线程的基本状态

Java线程运行的生命周期中只可能是6种不同的状态之一,在给定的一个时刻,线程只能处于其中的一个状态。

1、New状态:初始状态,线程被构建出来,但是还没有调用start()方法 2、 Runnable状态:运行状态,java线程把操作系统的就绪和运行两种状态统称为“运行中”。 3、 Blocked状态:阻塞状态,表示线程阻塞与锁。 4、 Waiting状态:等待状态,表示线程进入等待状态,进入该状态表示线程需要等待其他线程做出一些特定的动作或通知。 5、TIME WAITING状态:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 6、 TIME WAITING状态:终止状态,表示当前线程已经执行完毕

Java线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如下图所示: 在这里插入图片描述

线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。

实例测试:

public class ThreadState {
    public static void main(String[] args) {
        new Thread(new TimeWaiting(),"TimeWaitingThread").start();
        new Thread(new Waiting(),"WaitingThread").start();

        new Thread(new Blocked(),"Blocked-1").start();
        new Thread(new Blocked(),"Blocked-2").start();
    }
    // 这个线程不断地休眠
    static class TimeWaiting implements Runnable{
        @Override
        public void run() {
            while (true){
                SleepUtils.second(2000);
            }
        }
    }
    // 这个线程在Waiting.Class上等待
    static class Waiting implements Runnable{
        @Override
        public void run() {
            while (true){
                synchronized (Waiting.class){
                    try {
                        Waiting.class.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    // 该线程在Blocked.class实例上加锁后,不会释放该锁
    static class Blocked implements Runnable{
        @Override
        public void run() {
            synchronized (Blocked.class){
                while (true){
                    SleepUtils.second(1000);
                }
            }
        }
    }
}

代码2(放在同一个文件夹下运行)

import java.util.concurrent.TimeUnit;

public class SleepUtils {
    public static final void second(long second){
        try {
            TimeUnit.SECONDS.sleep(second);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

运行代码,打开终端或者命令提示符,键入“jps”,输出如下: 在这里插入图片描述 然后就可以看到运行实例对应的进程ID是177428,然后在命令行终端输入jstack 17748,部分终端输出如下: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

线程的几种创建方式

在这里插入图片描述

通过Thread类创建多线程

继承Thread类实现多线程有两种方法,一种是当做线程对象存在实现(也就是继承Thread类,并实现Thread对象),第二种是通过匿名内部类实现多线程。

作为线程对象存在(继承Thread对象)

	/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/25 20:50
 */
public class CreateThreadTest01 extends Thread{
    public CreateThreadTest01(String CreateName){
        super(CreateName);
    }
    @Override
    public void run(){
        while (!interrupted()){
            System.out.println(getName()+"线程执行了......");
            try{
                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        CreateThreadTest01 test01 = new CreateThreadTest01("Name1");
        CreateThreadTest01 test02 = new CreateThreadTest01("Name2");
        test01.start();
        test02.start();
        //test01.interrupt();//中断线程1
    }
}

结果:可以看出线程1和线程2不间断地交替打印。 在这里插入图片描述 当取消掉调用 interrupted方法的注释,可以用来判断该线程是否被中断。(终止线程不允许用stop方法,该方法不会施放占用的资源)。当使用了中断线程时,结果如下:线程1已经不能运行了,只能打印线程2. 在这里插入图片描述

匿名内部类创建线程对象

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/25 20:59
 */
public class CreateThreadTest03{
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run(){
                System.out.println("无参数线程创建成功!");
            }
        }.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("带线程任务的线程对象执行了");
            }
        }).start();
        // 创建带线程任务并且重写的run方法的线程对象
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("runnable run 线程执行了。。。。");
            }
        }){
            @Override
            public void run(){
                System.out.println("Override run 执行了");
            }
        }.start();
        // 调用的重写的方法应该是Thread类的run方法。而不是Runnable接口的run方法。
    }
}

结果: 在这里插入图片描述

实现runnable接口,作为线程任务存在

Runnable 接口只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放到一个线程对象里。 实例代码:

import static java.lang.Thread.interrupted;

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/25 20:55
 */
public class CreateThreadTest02 implements Runnable{
    @Override
    public void run() {
        while (!interrupted()){
            System.out.println("线程执行了......");
            try{
                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 将线程任务传递给线程对象
        Thread thread = new Thread(new CreateThreadTest02());
        //启动线程
        thread.start();
    }
}

结果: 程序每隔两秒打印一次 在这里插入图片描述

利用接口Callable创建带返回值的线程

实例代码:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/25 21:01
 */

public class CreateThreadTest04 implements Callable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CreateThreadTest04 teat04 = new CreateThreadTest04();
        // 最终实现的是Runnable的接口
        FutureTask<String> task = new FutureTask<String>(teat04);
        Thread thread = new Thread(task);
        thread.start();
        System.out.println("哈哈哈哈哈");
        String str = task.get();
        System.out.println("在线程的结果是:"+str);
    }
    @Override
    public Object call() throws Exception {
        Thread.sleep(2000);
        return "ABCD";
    }
}

结果: 在这里插入图片描述

使用定时器(Timer)

import java.util.Timer;
import java.util.TimerTask;

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/25 21:04
 */
public class CreateThreadTest05 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时器的线程执行了......");
            }
        },0,1000);
    }
}

结果: 在这里插入图片描述

线程池创建线程

线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

停止线程池

shutdown isShutdown:是否停止的状态,开始运行就是true isTerminated:线程是否完全停止 awaitTerminated:在一定时间内线程是否停止 返回boolean stutdownNow:立即停止,返回被中断的列表。

线程池的状态

RUNNING:接受新任务并处理排第任务。 SHUTDOWN:不接受新的任务,但处理排队任务。 STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务。 TIDYING:所有任务都已终止,workCont为零时,线程会转换到TIDTYING状态,并将运行terminate()的钩子方法。 TERMINATED:terminate()运行完成。

使用newSingleThreadExecutor创建线程

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

/**
 * @description 线程池创建:newSingleThreadExecutor()单一线程池
 *                        跟newFixedThreadPool()原理一样,只是把线程数和最大线程数设置为1
 */
public class SingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            executorService.execute(new FixedThreadPoolExecutor.Task());
        }
    }
}

创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CreateThreadTest06 {
    public static void main(String[] args) {
        // 创建具有5个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        long threadPoolStart= System.currentTimeMillis();
        for (int i = 0; i < 5; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"线程执行了......");
                }
            });
        }
        long threadPoolEnd = System.currentTimeMillis();
        System.out.println("创建5个线程用时:"+(threadPoolEnd-threadPoolStart));
        // 销毁线程池
        threadPool.shutdown();
    }
}

使用newFixedThreadPool创建线程

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

/**
 * @description 线程池创建:newFixedThreadPool()自定义线程数线程池
 *              由于传进去的LinkedBlockingQueue无界队列是没有队列上限的,所以当请求越来越多,并且无法及时处理完毕的时候,
 *              也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致OOM。
 */
public class FixedThreadPoolExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Task());
        }
        // 手动关闭线程
        executorService.shutdown();
    }
    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

使用newCachedThreadPool创建线程

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

/**
 * @description 线程池创建:newCachedThreadPool()可缓存线程池
 *                        特点:SynchronousQueue无界线程池,具有自动回收多余线程的功能
 *                        缺点:maxPoolSize是Integer.MAX_VALUE,可能会创建数量非常多线程,甚至导致OOM。
 */
public class CachedThreadPoolExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(new FixedThreadPoolExecutor.Task());
        }
    }
}

newScheduledThreadPool创建线程

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

/**
 * @description 线程池创建:newScheduledThreadPool() 支持定时及周期性任务执行的线程池
 */
public class ScheduledThreadPoolExecutor {
    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        //scheduledExecutorService.schedule(new FixedThreadPoolExecutor.Task(), 1, TimeUnit.SECONDS);
        // 每隔一秒一执行
        scheduledExecutorService.scheduleAtFixedRate(new FixedThreadPoolExecutor.Task(), 1 , 1, TimeUnit.SECONDS);
        Thread.sleep(5000);
        scheduledExecutorService.shutdown();
    }
}

停止运行的线程

线程在运行的过程中退出有如下几种方法:

1、使用退出标志,使得线程正常退出,就是说当run方法执行完毕后线程终止。 2、使用stop方法强制退出,但是不推荐这个方法 3、使用interrupt方法中断线程 4、程序运行结束,线程自动结束。

public class ThreadTest extends Thread{
    volatile boolean stop = false;
    public void run(){
        while (!stop){
            System.out.println(getName()+"线程正在运行中......");
            try{
                sleep(1000);
            }catch (InterruptedException e){
                System.out.println("wake up from block ......");
                stop = true;
                e.printStackTrace();
            }
        }
        System.out.println(getName()+"线程已经退出了......");
    }
}
class InterruptThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadTest threadTest = new ThreadTest();
        System.out.println("线程开始运行......");
        threadTest.start();
        Thread.sleep(3000);
        System.out.println("出现Interrupt的线程为:"+threadTest.getName());
        threadTest.stop = true;
        threadTest.interrupt();
        Thread.sleep(3000);
        System.out.println("主线程已经停止了......");
    }
}

运行结果为: 在这里插入图片描述

线程的优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。

public class Priority {
	private static volatile boolean notStart = true;
	private static volatile boolean notEnd = true;
public static void main(String[] args) throws Exception {
	List<Job> jobs = new ArrayList<Job>();
	for (int i = 0; i < 10; i++) {
		int priority = i < 5 Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
		Job job = new Job(priority);
		jobs.add(job);
		Thread thread = new Thread(job, "Thread:" + i);
		thread.setPriority(priority);
		thread.start();
	}
	notStart = false;
	TimeUnit.SECONDS.sleep(10);
	notEnd = false;
	for (Job job : jobs) {
		System.out.println("Job Priority : " + job.priority + ",
			Count : " + job.jobCount);
	}
}
static class Job implements Runnable {
	private int priority;
	private long jobCount;
	public Job(int priority) {
		this.priority = priority;
	}
	public void run() {
		while (notStart) {
			Thread.yield();
		}
		while (notEnd) {
			Thread.yield();
			jobCount++;
		}
	}
}
}

扩展阅读

Runnable接口和Callable接口的区别

首先查看一下Runnable接口的源代码,反正Runnable接口中只有一个方法,Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已; 在这里插入图片描述 其次再查看一下Callable接口中的源码,Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。 在这里插入图片描述 这其实是很有意义的,首先多线程的运行和单线程运行相比有很多复杂困难的问题,因为多线程运行充满了未知性,比如说某条线程是否运行,运行了多少时间,线程所期望的数据是否已经赋值完毕,Callable接口的泛型的Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。这是一个优势。

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

每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动⼀一个线程;

start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run() 方法体代码执行完毕,可以直接继续执行下面的代码;这时此线程是处于就绪状态,并没有运行。然后通过此 Thread 类调用方法 run() 来完成其运行状态;

run()方法称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。run() 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果直接调用 run(),其实就相当于是调用了一个普通函数而已,直接用 run() 方法必须等待 run() 方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start() 方法而不是 run() 方法。

sleep()方法和wait()方法的区别

首先sleep()方法可以在任何地方使用,而wait()方法只能在同步方法或者同步块中运行。对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。sleep()方法导致了程序暂停执行指定的时间,让出 cpu 给其他线程(其他CPU可以执行其他任务),但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。注意在调用 sleep()方法的过程中, 线程不会释放对象锁。当调用 wait()方法的时候,当前线程会暂时退出同步资源锁,同时会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

线程安全

什么是线程安全?当多线程运行了同一代码的时候,如果产生了不同的结果会怎么样?就好比如家里养的鸡下的蛋结果却孵出来一个老鹰,这怎么也显得不合适了,所以线程安全说白了就一句话,当多线程运行同一代码,不会产生不一样的结果。即代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。因为共享数据没有,那线程之间就互不影响。但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会产生不可预制的结果。

举一个简单的例子,本来你在银行存有1000块钱,你要取1500块钱,你去银行办理两个业务,取钱和存钱,假如你的两个业务是同步进行的,这个时候是不是你的钱就取不出来了?因为你的银行了就只有1000块钱,你取不了1500块钱,但是你的钱却是可以存进去。钱取不了,是不是你去银行办理业务的这个进程就废掉了?因为没有完全运行成功嘛。

确保线程安全

通过合理的时间调度,避开共享资源的存取冲突。另外,在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源,设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。

确保多线程安全的方法:

1、对非安全的代码进行加锁控制 2、使用线程安全的类 3、多线程并发情况下,线程共享的变量改为方法级的局部变量

线程安全在三个方面体现:

原子性: 提供互斥访问,同一时刻只能有一个线程对数据进行操作(atomic, synchronized); 可见性: 一个线程对主内存的修改可以及时地被其他线程看到,(synchronized、 volatile); 有序性: 一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱⽆无序,(happensbefore 原则)

共享数据

第一种:将共享数据封装到一个对象中,把这个共享数据所在的对象传递给不同的Runnable。

第二种:将这些Runnable对象作为某一个类的内部类,共享的数据作为外部类的成员变量,对共享数据的操作分配给外部类的方法来完成,以此实现对操作共享数据的互斥和通信,作为内部类的Runnable来操作外部类的方法,实现对数据的操作。

class shareData {
    private int x = 0;
    public synchronized void addX(){
        x++;
        System.out.println("X++:"+x);
    }
    public synchronized void subX(){
        x--;
        System.out.println("X--:"+x);
    }
}
public class ThreadsVisitData{
    public static shareData share = new shareData();
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    share.addX();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    share.subX();
                }
            }
        }).start();
    }
}

在这里插入图片描述

ThreadLocal

从名字可以看出这叫线程变量,意思是ThreadLocal中填充的的变量输入当前线程,该变量对其他线程而言是隔离的,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

作用: 让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)在任何方法中都可以轻松获取到该对象。

使用场景:

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。 2、线程间数据隔离 3、进行事务操作,用于存储线程事务信息。 4、数据库连接,Session会话管理。

在这里插入图片描述

场景举例1:每个线程需要一个独享的对象(通常是工具类如:SimpleDateFormat和Random)

/**
 * @description threadLocal用法1:SimpleDateFormat
 *              利用ThreadLocal,给每个线程分配自己的SimpleDateFormat对象,保证线程安全,高效率利用内存
 *
 *              使用场景:
 *              1.在ThreadLocal第一次get的时候把对象给初始化,重写initialValue()方法和返回值, 对象初始化时间可以由我们控制
 *              2.如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,用ThreadLocal.set直接放到我们ThreadLocal中,以便后续使用
 *
 *              ThreadLocal好处:
 *              1.达到线程安全
 *              2.不需要加锁,提高执行效率
 *              3.更高效的利用内存、节省创建对象的开销
 *              4.免去传参的繁琐
 */
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;

public class ThreadLocal01 {
    private static ExecutorService executorService = Executors.newFixedThreadPool(10);
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int index = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    String result = new ThreadLocal01().data(index);
                    System.out.println(result);
                }
            });
        }
        executorService.shutdown();
    }
    public String data(int index){
        Date date = new Date(1000 * index);
        SimpleDateFormat simpleDateFormat = ThreadSafeSimpleDateFormat.threadLocal2.get();
        return simpleDateFormat.format(date);
    }
}
class ThreadSafeSimpleDateFormat {
    public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
    public static ThreadLocal<SimpleDateFormat> threadLocal2 = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

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

/**
 * @description threadLocal用法2:每个线程内需要保持全局变量,可以让不同方法直接使用,避免参数传递的麻烦
 *                               防止内存泄露,用完里ThreadLocal里面值,要进行remove
 */
public class ThreadLocal02 {
    public static void main(String[] args) {
        Server1 server1 = new Server1();
        server1.process();
    }
}
class Server1{
    public void process(){
        User user = new User("张三");
        UserContextHolder.userThreadLocal.set(user);
        Server2 server2 = new Server2();
        server2.process();
    }
}
class Server2{
    public void process(){
        User user = UserContextHolder.userThreadLocal.get();
        System.out.println("Server2 获取用户名字:" + user.getName());
        UserContextHolder.userThreadLocal.remove();
        User newUser = new User("李四");
        UserContextHolder.userThreadLocal.set(newUser);
        Server3 server3 = new Server3();
        server3.process();
    }
}
class Server3{
    public void process(){
        User user = UserContextHolder.userThreadLocal.get();
        System.out.println("Server3 获取新用户名字:" + user.getName());
        // 用完后一定要remove
        UserContextHolder.userThreadLocal.remove();
    }
}
class User{
    private String name;
    public String getName() {
        return name;
    }
    public User(String name) {
        this.name = name;
    }
}
class UserContextHolder{
    public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
}

结果: 在这里插入图片描述