【并发编程系列 — 线程基础】

news2024/12/27 13:00:54

Java并发是Java语言中非常重要的一个话题,也是面试和实际应用中最常接触到的。作为JAVA工程师,掌握并发编程知识是非常重要的。在多核CPU的环境下,使用多线程并发执行任务可以显著提高程序的执行效率,从而提高系统的性能。

目录

线程的基本概念

线程和进程

线程创建的三种方式

线程的状态

线程的通知与等待

wait()方法

wait(long timeout)

notify()

notifyAll()

join()方法

sleep()

yield()

线程的上下文切换

线程死锁

死锁的产生的四个条件

如何避免死锁

守护线程和用户线程

ThreadLocal

使用示例

实现原理

ThreadLocalMap

ThreadLocal不支持继承性

InheritableThreadLocal类

总结

参考资料


线程的基本概念

线程和进程

        在操作系统中,一个进程就是一个正在运行的程序实例,它由程序代码和程序执行时所需的资源组成。一个进程可以包含多个线程,线程是进程中的一个执行路径,是CPU调度和分配的基本单位

        每个线程都有自己的程序计数器(PC)、栈、局部变量和方法参数。多个线程可以共享进程中的资源,例如全局变量、静态变量和方法等。多线程可以共享数据,从而使得程序更加灵活高效。

线程创建的三种方式

  • 继承Thread类,重写run方法
/**
 * 方式一:继承Thread类的方式创建线程
 */
public class ThreadExtendTest extends Thread{ //步骤 1
    @Override
    public void run() { //步骤 2
	    //run方法内为具体的逻辑实现
        System.out.println("create thread by thread extend");
    }
    public static void main(String[] args) {
        new ThreadExtendTest(). start();
    }
}

thread.start()后线程启动,转化为就绪态

        问题: Java是单继承的,如果继承了Thread类,就无法继承其他类;任务与代码没有分离,当多个线程执行同样的任务时需要多份任务代码;

  • 实现Runnable接口的run方法
/**
 * 方式二:实现java.lang.Runnable接口
 */
public class ThreadRunnableTest implements Runnable{//步骤 1
    @Override
    public void run() {//步骤 2
		//run方法内为具体的逻辑实现
        System.out.println("create thread by runnable implements");
    }
    public static void main(String[] args) {
        ThreadRunnableTest task = new ThreadRunnableTest();
        new Thread(task). start();
        new Thread(task). start();
   }
}

缺点:任务没有返回值

  • 使用FutureTask方式
/**
 * 方式三:实现Callable接口
 */
public class ThreadCallableTest implements Callable<String> {//步骤 1
    @Override
    public String call() throws Exception { //步骤 2
	    //call 方法的返回值类型是 String
	    //call 方法是线程具体逻辑的实现方法
        return "create thread by implements Callable";
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException{
        //创建异步任务
        FutureTask<String> future1 = new FutureTask<String>(new ThreadCallableTest());
        Thread thread1 = new Thread(future1);
        thread1. start();
        System.out.println(future1.get());
    }
}
  • 步骤 1:实现 Callable 接口,implements Callable
  • 步骤 2:复写 call() 方法,call()方法是线程具体逻辑的实现方法。

小结

  • 使用继承的方式可以方便传参,可以在子类中添加成员变量,但子类无法继承其他类
  • 使用Runnable方式,则只能使用主线程中被final修饰的变量
  • FutureTask方式能够获取返回结果

线程的状态

线程在不同的生命周期中会有不同的状态。JAVA中的线程有以下五种状态:

  • 新建(New):线程对象被创建后,但还没有调用start()方法启动线程时的状态。
  • 运行(Runnable):线程被start()方法启动后,进入可运行状态。在可运行状态中,线程正在等待CPU的调度,以便进入运行状态。
  • 阻塞(Blocked):线程在等待某个条件时,进入阻塞状态。例如等待I/O操作完成,等待锁释放等。
  • 等待(Waiting):线程等待其他线程执行完特定的操作,进入等待状态。例如等待某个线程完成、等待某个对象的notify()或notifyAll()方法。
  • 终止(Terminated):线程运行完run()方法后,线程会进入终止状态。线程的状态转换如下图所示:

线程的通知与等待

wait()方法

        wait()是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用wait()方法。当一个线程调用一个共享变量的wait()方法时, 该调用线程会被阻塞挂起释放锁并进入等待状态, 直到发生下面几件事情之一才返回:

  • 其他线程调用了该共享对象(同一个监视器)notify()或者notifyAll() 方法;
  • 其他线程调用了该线程的interrupt() 方法, 该线程抛InterruptedException 异常返回。

另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException 异常。

如何获取一个共享变量的监视器锁?

  1. 执行synchronized 同步代码块时, 使用该共享变量作为参数
synchronized (共享变量){
	//doSomething
}

        2. 调用该共享变量的方法,并且该方法使用了synchronized 修饰。

synchronized void add ( int a , int b ) {
	//doSomething
}

注:synchronized 修饰方法锁的是什么,取决于方法是静态的还是非静态的。

  • 如果synchronized 修饰非静态方法,那么锁的是调用该方法的对象,也就是说,只有同一个对象的同步方法才会互相阻塞,不同对象的同步方法可以同时执行。这种锁又叫对象锁
  • 如果synchronized 修饰静态方法,那么锁的是该类的Class对象,也就是说,只有同一个类的静态同步方法才会互相阻塞,不同类的静态同步方法可以同时执行。这种锁又叫类锁

所以上面synchronized修饰方法锁的不是共享变量,而是调用方法的对象。

例:假设有两个线程A和B,A负责生产数据,B负责消费数据。A和B共享一个队列queue作为数据缓冲区。当队列为空时,B需要等待A生产数据;当队列满时,A需要等待B消费数据。

        其中queue 为共享变量,生产者线程在调用queuewait()方法前,使用synchronized 关键字拿到了该共享变量queue 的监视器锁,所以调用wait() 方法才不会抛出IllegalMonitorStateException 异常。如果当前队列没有空闲容量则会调用queuewait() 方法挂起当前线程。

注:释放的是当前共享变量上的锁,如果还持有其他锁,这些锁并不会被释放

// A thread that produces data and puts it on the queue
//生产线程
class Producer extends Thread {
    private Queue queue;

    public Producer(Queue queue) {
        this.queue = queue;
    }

    public void run() {
        while (true) {
            synchronized (queue) {
                // wait if the queue is full
                //消费队列满,则等待队列空闲
                while (queue.isFull()) {
                    try {
                        //挂起当前线程, 并释放通过同步块获取的queue上的锁,
                        //让消费者线程可以获取该锁,然后获取队列里面的元素
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // produce data and notify the consumer
                // 空闲则生成元素, 并通知消费者线程
                Object data = produceData();
                queue.put(data);
                queue.notify();
            }
        }
    }

    private Object produceData() {
        // some logic to produce data
        return new Object();
    }
}

// A thread that consumes data from the queue
//消费者线程
class Consumer extends Thread {
    private Queue queue;

    public Consumer(Queue queue) {
        this.queue = queue;
    }

    public void run() {
        while (true) {
            synchronized (queue) {
                // wait if the queue is empty
                // 消费队列为空
                while (queue.isEmpty()) {
                    try {
                        //挂起当前线程,并释放通过同步块获取的queue上的锁,
                        //让生产者线程可以获取该锁,将生产元素放入队列
                        queue.wait();
                    } catch (InterruptedException) {
                        e.printStackTrace();
                    }
                }

                //消费元素,并通知唤醒生产者线程
				queue.take( );
				queue.notify();
            }
        }
    }

这里使用循环就是为了避免虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量,那么当前线程还是会调用wait()方法把自己挂起。

虚假唤醒可参考博客:什么是虚假唤醒?为什么会产生虚假唤醒?_橙不甜橘不酸的博客-CSDN博客

wait(long timeout)

        该方法相比wait() 方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后, 没有在指定的timeout ms 时间内被其他线程调用该共享变量的notify()或者notifyAll() 方法唤醒,那么该函数还是会因为超时而返回 。如果将timeout 设置为0 则和wait 方法效果一样,因为在wait 方法内部就是调用了wait(0) 。需要注意的是,如果在调用该函数时, 传递了一个负的timeout 则会抛出IllegalArgumentException 异常。

notify()

        一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

        被唤醒的线程不能马上从wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁 , 只有该线程竞争到了共享变量的监视器锁后才可以继续执行

类似wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify() 方法,否则会抛出IllegalMonitorStateException 异常。

notifyAll()

        不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll() 方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

join()方法

        在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行, 比如多个线程加载资源, 需要等待多个线程全部加载完毕再汇总处理Thread 类中有一个join 方法就可以做这个事情,前面介绍的等待通知方法是Object 类中的方法, 而join方法则是Thread 类直接提供的。join 是无参且返回值为void 的方法。

public static void main(Str 工ng[] args) throws InterruptedException {
    Thread threadOne = new Thread(new Runnable( ) {
        @Override
        public void run() {
            try {
            	Thread.sleep (1000);
            ) catch (Interrupted.Exceptio e) {
            	e.printStackTrace();
            }
            System.out.println("child threadOne over !");
        }
    });
    Thread threadTwo = new Thread(new Runnable() {
        @Override
         public void run(){
            try {
            	Thread.sleep (1000);
            ) catch (Interrupted.Exceptio e) {
            	e.printStackTrace();
            }
        	System.out.println( "child threadTwo over") ;
        }
    });

	//启动子线程
    threadOne.start() ;
    threadTwo.start() ;
    System.out.println("wait all child thread over !");
    //等待子线程执行完毕,返回
    threadOne.join ();
    threadTwo.join ();

	System.out.println ( " all child thread over ") ;
}

如上代码在主线程里面启动了两个子线程,然后分别调用了它们的join()方法,那么主线程首先会在调用threadOne.join() 方法后被阻塞, 等待threadOne 执行完毕后返回。

threadOne 执行完毕后threadOne.join() 就会返回, 然后主线程调用threadTwo.join()方法后再次被阻塞, 等待threadTwo 执行完毕后返回。

sleep()

  Thread类中有一个静态的sleep 方法,当一个执行中的线程调用了Threadsleep 方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU 的调度,获取到CPU 资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep 方法的地方抛出IntermptedException 异常而返回。

例:

public class SleepTest {
	// 创建一个独占锁
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new Runnable(){
            public void run(){
                lock.lock(); // 获取独占锁
                try{
                    System.out.println("child threadA is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadA is awaked");
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock(); // 释放锁
                }
            }
        });

        Thread threadB = new Thread(new Runnable(){
            public void run(){
                lock.lock(); // 获取独占锁
                try{
                    System.out.println("child threadB is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadB is awaked");
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock(); // 释放锁
                }
            }
        });

        // 启动线程
        threadA.start();
        threadB.start();
    }
}

运行结果:

child threadA is in sleep
child threadA is awaked
child threadB is in sleep
child threadB is awaked

在线程A 睡眠的这10 s 内那个独占锁lock 还是线程A 自己持有,线程B 会一直阻塞直到线程A 醒来后执行unlock 释放锁。

yield()

        Thread 类中有一个静态的yield 方法,当一个线程调用yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU 使用但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为每个线程分配一个时间片来占有CPU 的, 正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread 类的静态方法yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

        当一个线程调用yield 方法时, 当前线程会让出CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU 的那个线程来获取CPU 执行权。

例:

public class YieldTest implements Runnable {
    YieldTest() {
        //创建并启动线斗呈
        Thread t =new Thread(this);
        t.start() ;
    }
    
    public void run() {
        for (int i = 0 ; i < 5; i++) {
        	//当i=0 时让出CPU执行权,放弃时间片,进行下一轮调度
            if ((i % 5) == 0 ) {
           	 	System.out.println (Thread.currentThread() + "yield cpu . . .");
                //当前线程让出CPU执行权,放弃时间片,进行下一轮调度
                //Thread.yield ();
            }
        }
        
        System .out . println(Thread.currentThread() + " is over ");
    }
    
    public static void main(String [] args) {
        new YieldTest();
        new YieldTest();
        new YieldTest();
    }
}

输出结果:

Thread[Thread-0,5 ,main]yield cpu ...
Thread[Thread-0,5 ,main] is over
Thread[Thread-1,5 ,main]yield cpu ...
Thread[Thread-1,5,main] is over
Thread[Thread-2,5 ,main]yield cpu ...
Thread[Thread-2,5 ,main] is over

如上代码开启了三个线程,每个线程的功能都一样,都是在for 循环中执行5 次打印。运行多次后,上面的结果是出现次数最多的。解开Thread.yield()注释再执行,结果如下:

Thread[Thread-0,5 ,main]yield cpu ...
Thread[Thread-2,5 ,main]yield cpu ...
Thread[Thread-1,5 ,main]yield cpu ...
Thread[Thread-0,5 ,main] is over
Thread[Thread-2,5,main] is over
Thread[Thread-1,5 ,main] is over

从结果可知, Thread .yield() 方法生效了,三个线程分别在i=0 时调用了Thread. yield()方法,所以三个线程自己的两行输出没有在-起,因为输出了第一行后当前线程让出了CPU 执行权。

yield() 与sleep()的区别

        当线程调用sleep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield 方法时,线程只是让出自己剩余的时间片并没有被阻塞挂起而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

线程的上下文切换

        在多线程编程中,线程个数一般都大于CPU 个数,而每个CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU 执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU 让其他线程占用, 这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU 的线程等下次轮到自己占有CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场, 当再次执行时根据保存的执行现场信息恢复执行现场。

        线程上下文切换时机有: 当前线程的CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。

线程死锁

        死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如图所示,线程A 己经持有了资源2 , 它同时还想申请资源1 , 线程B 已经持有了资源1,它同时还想申请资源2 , 所以线程A和线程B就因为相互等待对方已经持有的资源,而进入了死锁状态。

死锁的产生的四个条件

  • 互斥条件: 线程对己经获取到的资源进行排它性使用, 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件: 一个线程己经持有了至少一个资源, 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。
  • 不可剥夺条件: 线程获取到的资源在自己使用完之前不能被其他线程抢占, 只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件: 指在发生死锁时, 必然存在一个线程→资源的环形链, 即线程集合{P0 , P1,P2 ,…, Pn }中的P0 正在等待一个P1 占用的资源, P1 正在等待P2 占用的资源,……Pn 正在等待己被P0 占用的资源。

死锁产生的原因

  • 进程顺序不当
  • PV操作使用不妥
  • 同类资源分配不均
  • 对某些资源的使用未加限制

……

如何避免死锁

        既然死锁要满足四个条件,那破坏掉至少一个就可以避免死锁了。但四个条件中,只有请求与保持和环路等待条件可以被破坏(互斥条件是必须要有的,不然多个线程同时操作一个共享资源,会出现脏读等问题,一般资源是不可剥夺的,当然也可能会有可剥夺的情况,这里不做考虑)

死锁具体相关的知识点可参考下面这篇文章,我就不赘述了。

​死锁的产生、防止、避免、检测和解除 - 知乎

守护线程和用户线程

        Java 中的线程分为两类,分别为daemon 线程(守护线程〉和user 线程(用户线程)。在JVM 启动时会调用main 函数, main 函数所在的钱程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程, 比如垃圾回收线程。那么守护线程和用户线程有什么区别呢?

        区别之一是当最后一个非守护线程结束时, JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM 的退出。言外之意,只要有一个用户线程还没结束, 正常情况下JVM 就不会退出。

如何创建守护线程?

public static void main(String[] args ) {
    Thread daemonThread = new Thread (new Runnable() {
        public void run( ) {
        }
    });
    //设置为守护线程
    daemonThread.setDaemon(true) ;
    daemonThread.start();
}

用户线程与守护线程的区别

public static vroid main (String [] args ) {
    Thread thread= new Thread(new Runnable() {
        publiC vroid run(){
        	for(;;) {}
        }
    });
    //启动子线程
    thread.start() ;
	System.out.print ( "main thread is over ");
}

输出结果:

        如上代码在main 线程中创建了一个thread 线程,在thread 线程里面是一个无限循环。从运行代码的结果看, main 线程已经运行结束了,那么JVM 进程己经退出了吗?在IDE的输出结果右上侧的红色方块说明, JVM 进程并没有退出。

        这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM 进程并不会终止。

那么我们把上面的thread 线程设置为守护线程后,再来运行看看会有什么结果:

//设置为守护线程
thread.setDaemo 口( true) ;
//启动子线程
thread.start();

输出结果:

在启动线程前将线程设置为守护线程,执行后的输出结果显示,JVM 进程己经终止了,执行ps -eaf | grep java 也看不到JVM 进程了。

        在这个例子中, main 函数是唯一的用户线程, thread 线程是守护线程,当main 线程运行结束后, JVM 发现当前己经没有用户线程了,就会终止JVM 进程。由于这里的守护线程执行的任务是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则JVM不等守护线程运行完毕就会结束JVM 进程。

        main 线程运行结束后, JVM 会自动启动一个叫作DestroyJavaVM 的线程, 该线程会等待所有用户线程结束后终止JVM 进程。

ThreadLocal

        在多线程编程中,线程间数据共享是非常常见的问题,但是容易出现并发问题,特别是多个线程对同一共享变量进行写入时。为了保证线程安全,一般使用者在访问成员变量时需要进行适当的同步

        我们通常会使用synchronized或者Lock等加锁方式进行线程同步,但是这些方式在性能方面是有一定的损耗的。那么有没有一种方式可以在保证线程安全的前提下,又不会带来太大的性能问题呢?ThreadLocal就是这样一种机制。

  ThreadLocal是一种线程封闭的机制,它可以将数据隔离在每个线程中,每个线程都拥有一份独立的数据副本, 实际操作的是自己本地内存的变量。这样一来,不同的线程之间就不会出现数据共享的问题了。

使用示例

public class ThreadLocalDemo {
    //创建ThreadLocal变量
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //创建线程
        new Thread(() -> {
            //设置threadLocal本地变量的值
            threadLocal.set("hello world");
            //使用get方法获取该值
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        }, "thread1").start();

        //创建线程
        new Thread(() -> {
            threadLocal.set("你好 世界");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        }, "thread2").start();
    }
}

在这个示例代码中,我们创建了两个线程,分别向ThreadLocal中设置了不同的值。由于ThreadLocal是线程封闭的机制,因此每个线程都拥有自己的独立的数据副本,两个线程之间不会互相影响。输出结果如下:

thread1 hello world
thread2 你好 世界

实现原理

ThreadLocal的实现原理其实比较简单,它主要依赖于ThreadLocalMap字段实现,其类图结构如下:

ThreadLocal的类结构比较简单,主要包含以下几个方法:

  • set:向当前线程的ThreadLocalMap中存储值
  • get:获取当前线程的ThreadLocalMap中的值
  • remove:清除当前线程的ThreadLocalMap中的值
  • initialValue:初始化值,可在子类中覆盖此方法以提供默认值

ThreadLocal源码

public class ThreadLocal<T> {
    /**
* 返回当前线程对应的ThreadLocalMap。
* 这里的threadLocals是ThreadLocalMap类型的变量,作为存储多个ThreadLocal变量的容器,后面再具体解析
*/
    private ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
* 在当前线程的ThreadLocalMap中设置值。
*/
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //更新当前ThreadLocal变量的值,注意,这里this值ThreadLocal类的当前实例对象
            map.set(this, value);
        else
            //第一次调用就创建当前线程对应的HashMap
            createMap(t, value);
    }

    /**
* 从当前线程的ThreadLocalMap中获取值。
*/
    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //将当前线程作为key,查找对应的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //通过this:当前实例对象获取ThreadLocal
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T) e.value;
        }
        //ThreadLocalMap为空则初始化当前线程的ThreadLocal变量值
        return setInitialValue();
    }

    /**
* 从当前线程的ThreadLocalMap中清除值。
*/
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }

    /**
* 初始化值。
*/
    protected T initialValue() {
        return null;
    }

    /**
* 创建一个新的ThreadLocalMap并初始化值。
*/
    void createMap(Thread t, T firstValue) {
        //传入的key值是当前ThreadLocal的this引用
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    /**
* 获取初始值,如果子类未提供初始值,则返回null。
*/
    T setInitialValue() {
        //初始化为空
        T value = initialValue();
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            //第一次调用就创建当前线程对应的HashMap
            createMap(t, value);
        return value;
    }
}

注:一开始被ThreadLocalthreadLocalsThreadLocalMap搞晕了,后面发现其实很简单,直接把threadLocals给踢了,只剩下ThreadLocalThreadLocalMap,这样就好理解多了。threadLocals这玩意只是个变量名而已,不用关心。

记住ThreadLocal类对应一个变量,里面有一些对应的方法,而ThreadLocalMap是一个Map,里面有很多ThreadLocal, 取的时候通过当前线程和ThreadLocal对象的哈希码,找到对应的Entry对象,然后返回它的value字段。

ThreadLocal类中,主要是通过对当前线程的ThreadLocalMap的操作来实现线程本地变量的存储和获取,具体实现细节可以通过源码来了解。

ThreadLocalMap

        Thread类中有一个ThreadLocalMap类型的字段threadLocals,它是线程本地变量的存储容器。每个ThreadLocal对象都有一个对应的entry在threadLocals中 ,用来存储线程对应的值。

  ThreadLocalMap是一个自定义的HashMap,它的键为ThreadLocal对象,值为线程保存的数据对象。在每个线程中, ThreadLocalMap 都是独立的,因此不同的线程之间不会出现数据共享的问题。由于ThreadLocalMap是线程独立的,因此在多线程环境下不会出现竞争的问题,从而保证了线程安全性。

  ThreadLocalMap中的键值对的生命周期是跟随线程的,当线程结束后,ThreadLocalMap中的键值对也会被回收。如何线程一直不消亡,会导致这些本地变量一直存在,所以可能造成内存溢出,因此,使用结束后记得要调用ThreadLocalremove方法删除对应线程ThreadLocalMap中存储的本地变量。

  ThreadLocal对象通过调用ThreadLocalset方法将值存储到当前线程的ThreadLocalMap中,通过调用ThreadLocalget方法可以获取当前线程的值。ThreadLocalremove 方法可以清除当前线程中的ThreadLocalMap中的键值对,从而释放资源。

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

        在Thread类中使用到两个变量:threadLocalsinheritableThreadLocals ,都是 ThreadLocalMap类型的变量。

  ThreadLocals是线程本地变量的存储容器。每个ThreadLocal对象都有一个对应的entry在ThreadLocals中,用来存储线程对应的值。这里也就刚好引出另一个问题了:为什么threadLocals被设计成map结构

        很明显,ThreadLocals要作为容器存储多个ThreadLocal本地变量,并通过每个线程进行关联,自然我们就想到了map了。

ThreadLocal不支持继承性

  ThreadLocal不支持继承性,也就是说,子线程无法访问父线程中的ThreadLocal变量,因为子线程中的ThreadLocal变量是子线程自己的,而不是从父线程中继承的。

        举个例子,假设有一个父线程和一个子线程,父线程中有一个ThreadLocal变量,当父线程启动子线程时,子线程无法访问父线程中的ThreadLocal变量,因为子线程中的ThreadLocal变量是子线程自己的,而不是从父线程中继承的。

public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("Hello, World!");

        Thread thread = new Thread(() -> {
            System.out.println("Child thread value: " + threadLocal.get());
        });
        thread.start();

        System.out.println("Main thread value: " + threadLocal.get());
    }
}

运行结果如下:

Main thread value: Hello, World!
Child thread value: null

可以看到,子线程中的ThreadLocal变量值为null,而不是从父线程中继承的值。

那有没有办法可以让子线程能够访问到父线程中的值呢?当然!

InheritableThreadLocal类

InheritableThreadLocal类是Java中的一个线程本地变量类,它继承了ThreadLocal类,扩展了ThreadLocal以提供从父线程到子线程的值继承。

下面是一个使用InheritableThreadLocal类的例子:

public class InheritableThreadLocalExample {
    public static void main(String[] args) {
        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        inheritableThreadLocal.set("Hello World");

        Thread thread = new Thread(() -> {
            System.out.println(inheritableThreadLocal.get());
        });

        thread.start();
    }
}

        在这个例子中,我们创建了一个InheritableThreadLocal对象,然后在主线程中设置了一个值。接着,我们创建了一个子线程,并在子线程中获取了这个值。由于InheritableThreadLocal类提供了从父线程到子线程的值继承,所以子线程中可以获取到这个值,输出结果为"Hello World"。

使用场景

  InheritableThreadLocal类的应用场景是当线程需要从父线程继承某些值时,可以使用InheritableThreadLocal类。例如,当需要在多个线程之间共享用户ID或事务ID等线程本地变量时,可以使用InheritableThreadLocal类。InheritableThreadLocal类提供了从父线程到子线程的值继承,因此子线程可以获取父线程中的值。

源码分析

Thread类中定义了叫inheritableThreadLocals的变量,类型为ThreadLocalMap

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal类中继承了ThreadLocal,并重写了三个方法,替换了原有的threadLocals

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
* 用于在子线程中创建一个新的InheritableThreadLocal对象时,为该对象提供一个初始值。
* 在默认情况下,该方法返回父线程中的值,因此子线程中的InheritableThreadLocal对象的初始值与父线程中的值相同。
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
* 获取的是inheritableThreadLocals变量
* 而不再是ThreadLocals变量
* @param t the current thread
*/
    ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
    }

    /**
* 创建当前线程的inheritableThreadLocals变量实例
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

再具体看一下childValue方法的执行流程,以及如何访问父线程的本地变量

当我们在主线程中start一个子线程时,会new 一个Thread。创建线程时发生了什么才让父子线程的InheritableThreadLocal可以传递?

在Thread类中,有多个默认构造方法, 经过重载,都最终调用了init方法

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}


//最终调用
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {


    //获取当前线程(父线程)
    Thread parent = currentThread();
    ……
    /* 如果inheritThreadLocals为true并且父线程中存在InheritableThreadLocal对象,
* 则使用ThreadLocal.createInheritedMap方法创建一个新的Map对象,
* 该对象包含父线程中所有InheritableThreadLocal对象的值。这个新的Map对象将作为
* 子线程的InheritableThreadLocalMap对象的初始值。
*/
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

}



//inheritThreadLocals变量的值复制一份到新的//创建一个新的Map对象
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}


private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    //将父线程的inheritThreadLocals变量的值复制一份到新的对象中
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //这里调用的是InheritableThreadLocal重写后的childValue方法
                Object value = key.childValue(e.value);//返回e.value
                Entry c = new Entry(key, value);
                //计算hash值
                int h = key.threadLocalHashCode & (len - 1);
                //找到一个空的位置添加Entry实例,就是hashmap添加元素的过程
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

总结

  ThreadLocal是一种线程封闭的机制,它可以将数据隔离在每个线程中,每个线程都拥有一份独立的数据副本。ThreadLocal主要是通过ThreadLocalMap和Thread类中的ThreadLocalMap字段实现,ThreadLocalMap是一个自定义的HashMap,用来存储线程本地变量的键值对,而Thread类中的ThreadLocalMap字段threadLocals则是用来存储每个线程的ThreadLocalMap。通过使用ThreadLocal,我们可以在保证线程安全的前提下,又不会带来太大的性能问题。

参考资料

《Java并发编程之美》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/507075.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

6.事件系统

事件 事件的传递 新建Widget项目myevent&#xff0c;类名WIdget不变 新建C类如下&#xff1a; 粘贴代码 1myevent运行结果&#xff1a; 输入任意字符时&#xff0c;qDebug显示如下&#xff0c;但框内无法显示输入的字符 //mylineedit.cpp中void MyLineEdit::keyPressEven…

[Docker]Docker命令

Docker常用命令 帮助命令 docker version #版本信息 docker info #详细信息 docker 命令 --help #万能命令镜像命令&#xff1a; docker images #展示所有镜像 docker search docker pull 下载镜像 docker rmi 删除镜像 &#xff08;remove images) 容器命令…

数据分析---Python科学计算库-Numpy学习(一)

一&#xff0c;简介 1.1什么是数据分析 数据分析是指通过使用各种统计学、计算机科学和数学技术&#xff0c;对收集来的数据进行处理、整理、解释和呈现&#xff0c;从而提取出数据中有用的信息和洞察&#xff0c;并帮助做出更明智的决策。数据分析可以应用于各种不同的领域和…

MapReduce源码解读-2

MapReduce源码解读-2 MapReduceInputFormat类getSplitsCreateRecordReader Mapper类OutputCollectorMapOutputBufferPartitioner 分区环形缓冲区 Circular buffer初始化 Spill、Sort溢写、排序Merge 合并combiner 规约 Reduce阶段ReduceTask第一层调用 ReduceTask.run -shuffle…

【力扣】刷题+剑指offer第二版

文章目录 题目收藏不含重复字符的最长子串最长公共子串 剑指 Offer剑指 Offer 05. 替换空格剑指 Offer 03. 数组中重复的数字剑指 Offer 04. 二维数组中的查找剑指 Offer 09. 用两个栈实现队列剑指 Offer 07. 重建二叉树剑指 Offer 06. 从尾到头打印链表剑指 Offer 11. 旋转数组…

【大数据之Hadoop】二十八、生产调优-HDFS集群扩容及缩容

增加或缩减服务器&#xff0c;注意不允许白名单和黑名单同时出现同一个主机。 1 服役新服务器 原有数据节点不能满足数据存储需求时&#xff0c;需要在原有集群的基础上动态增加节点&#xff0c;即动态增加服务器&#xff0c;增加服务器的同时不需要重启集群。 hadoop完全分布…

JVM相关知识点

java内存区域 线程私有的&#xff1a; 程序计数器虚拟机栈本地方法栈 线程共享的&#xff1a; 堆方法区直接内存 程序计数器&#xff1a;记录当前线程执行的位置 当线程切换后能够知道该线程上次运行到哪了 java虚拟机栈&#xff1a; 方法调用的数据通过栈进行传递&#…

一篇文章带你重新回溯单链表的所有

&#x1f349;博客主页&#xff1a;阿博历练记 &#x1f4d7;文章专栏&#xff1a;数据结构与算法 &#x1f69a;代码仓库&#xff1a;阿博编程日记 &#x1f339;欢迎关注&#xff1a;欢迎友友们订阅收藏关注哦 文章目录 &#x1f34c;前言&#x1f4bb;无头单向非循环链表&am…

SSM(Spring、SpringMVC、MyBatis)整合、配置

SpringMVC是一个表述层(前台的页面和后台的servlet)框架&#xff0c;处理浏览器发送到服务器的请求&#xff0c;并且将一些数据响应到浏览器 MyBatis是一个持久层框架&#xff0c;帮助我们连接数据库&#xff0c;访问数据库&#xff0c;操作数据库中的数据 Spring是一个整合型框…

毕业论文相关

毕业论文参考文献和Word保存 一、Word中出现[7-9]多个文献的引用 在正文中选中参考文献角标&#xff0c;右击选择“切换域代码”&#xff0c;参考文献角标[7][8][9]变为{ REF _Ref98345319 \r \h * MERGEFORMAT }{ REF _Ref98345321 \r \h * MERGEFORMAT }{ REF _Ref99390603…

AQS独占锁之ReentrantLock源码调试(JDK8)

前言&#xff1a; 为什么需要学习ReentrantLock? 目前项目开发中使用到的几乎都是分布式锁&#xff0c;平时可能很少用到java自带的锁&#xff1b; 但实际在我们java的源码中&#xff0c;随处可见需要使用锁来保证线程安全&#xff0c;所以还是有必要学习下ReentrantLock。 1…

7.分区表和分桶表

1.创建分区表 create table dept_partition(deptno int,dname string,loc int ) partitioned by (dt string) // 分区字段(date) row format delimited fields terminated by \t; 2.增删改查操作 2.1 插入数据 1&#xff09;导入本地数据 -- 创建一个名字为dt2022-06-14的…

R语言 | 输入与输出

目录 一、认识文件夹 1.1 getwd()函数 1.2 setwd()函数 1.3 file.path()函数 1.4 dir()函数 1.5 list.files()函数 1.6 file.exists()函数 1.7 file.rename()函数 1.8 file.create()函数 1.9 file.copy()函数 ​1.10 file.remove()函数 二、数据输出&#xff1a;ca…

单片机c51中断 — 中断扫描法行列式键盘

项目文件 文件 关于项目的内容知识点可以见专栏单片机原理及应用 的第五章&#xff0c;中断 在第4章中已介绍过行列式键盘的工作原理&#xff0c;并编写了相应的键盘扫描程序。但应注意的是&#xff0c;在单片机应用系统中&#xff0c;键盘扫描只是 CPU 工作的内容之一。CPU …

一文理清 TiDB 与 MySQL 中的常用字符集及排序规则

1. 字符集&#xff08;character set&#xff09; 1.1. 字符集与编码规则 字符集&#xff08;character set&#xff09;即为众多字符的集合。字符集为每个字符分配一个唯一的 ID&#xff0c;称为 “Code Point&#xff08;码点&#xff09;”。编码规则是将 Code Point 转换…

商户查询的缓存——缓存击穿问题

缓存击穿问题也叫热点key问题&#xff0c;就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效了&#xff0c;无数的请求访问会在瞬间给数据库带来巨大的冲击 常见的解决方案有两种&#xff1a; 互斥锁&#xff08;高并发时性能较差&#xff09; 逻辑过期 基于互斥锁方…

ASN.1-PKCS10-x509

在国际标准ITU-T X.690 《Information technology – ASN.1 encoding rules: Specification of Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and Distinguished Encoding Rules (DER)》中定义了ASN.1编码规则。对于一般数据类型&#xff08;比如Integer、octe…

【软件工程】自动化测试保证卓越软件工程能力(2)

本次内容我们抽象一个待测试的目标软件产品&#xff0c;产品是基于web开发的。 自动化平台不是独立存在的&#xff0c;必然有一个目标待测试产品&#xff0c;用自动化测试来反映产品功能是否还是好的。 产品抽象v1 第一个版本&#xff0c;使用者&#xff08;USER&#xff09;发…

配置本地Angular环境并使用VsCode调试Angular前端项目

配置本地Angular环境并使用VsCode调试Angular前端项目 配置本地Angular环境部署Node.Js本地环境配置一下环境变量 使用vscode调试Angular安装vscode 配置本地Angular环境 部署Node.Js本地环境 1 从官网下载node.js, 本文为(v16.13.0) 下载地址: https://nodejs.org/dist/v16.…

windows server 2016报错无法打开所需文件install.wim

报错的前提条件: 1.下载原版镜像后,使用UltraISO制作U盘系统盘。 2.正常安装系统,到“安装程序正在启动界面”时弹出错误窗口,报错“Windows无法打开所需的文件 E:\Source\install.win。请确保安装所需的所有文件可用,并重新启动安装。错误代码:0x80070026”。 问题原因…