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

线程同步、死锁与生产者消费者

一、问题引出

多个线程访问同一个资源时,如果操作不当就很容易产生意想不到的错误,比如常见的抢票程序:

public class Demo1 {
    public static void main(String[] args) {
        Ticket tt = new Ticket();
        new Thread(tt, "甲").start();
        new Thread(tt, "乙").start();
    }
}

class Ticket implements Runnable {
    private int ticketCount = 10;
    @Override
    public void run() {
        while (ticketCount > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第【" + (10 - ticketCount + 1) + "】张火车票");
            ticketCount--;
        }
    }
}

结果:

乙抢到了第【1】张火车票
甲抢到了第【2】张火车票
乙抢到了第【3】张火车票
甲抢到了第【4】张火车票
乙抢到了第【5】张火车票
甲抢到了第【6】张火车票
乙抢到了第【7】张火车票
甲抢到了第【8】张火车票
乙抢到了第【9】张火车票
甲抢到了第【9】张火车票
乙抢到了第【11】张火车票

以上代码可以看出,第9张票被两个人抢,且出现了第11张票,这不符合实际。为什么会出现这种现象?原因是当乙抢到第9张票但还未执行ticketCount–;语句时,甲也进入了run()方法,此时票数仍然是第9张票,也打印出了第9票,因此第9张票被抢了两遍。之后甲和乙都会执行ticketCount–;语句,当ticketCount =0时,无论哪个线程抢到资源都会打印抢到了第【11】张火车票,当ticketCount = -1时,停止。

综上分析,问题出现的主要原因就是甲、乙两个线程同时访问同一资源,造成了资源污染。

二、线程同步

上面可知当多个线程同时访问同一资源,就可能会造成了资源污染,解决这个问题的方法就是在某个线程访问资源时,其他线程在资源或者方法外面等待,也就是线程同步,或者叫加锁。
线程同步就是指多个操作在同一时间段内只能有一个线程进行,而其他线程要等待此线程完成之后才可以继续进行。
要实现线程同步,需要通过关键字synchronized关键字,利用这个关键字可以定义同步方法或者代码块。格式如下:

synchronized(同步对象){
	操作;
}

一般要进行同步对象处理时,采用当前对象this进行同步。上面的抢票代码加上同步后如下:

public class Demo1 {
    public static void main(String[] args) {
        Ticket tt = new Ticket();
        new Thread(tt, "甲").start();
        new Thread(tt, "乙").start();
    }
}

class Ticket implements Runnable {
    private int ticketCount = 10;
    @Override
    public void run() {
        synchronized(this){
            while (ticketCount > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "抢到了第【" + (10 - ticketCount + 1) + "】张火车票");
                ticketCount--;
            }
        }
    }
}

结果:

甲抢到了第【1】张火车票
甲抢到了第【2】张火车票
甲抢到了第【3】张火车票
甲抢到了第【4】张火车票
甲抢到了第【5】张火车票
甲抢到了第【6】张火车票
甲抢到了第【7】张火车票
甲抢到了第【8】张火车票
甲抢到了第【9】张火车票
甲抢到了第【10】张火车票

加锁或者同步处理以后,虽然多线程同时访问同一资源的问题虽然解决了,但是线程同步会降低整体性能。

三、死锁

死锁就是多个线程间相互等待的状态。

class MyThread implements Runnable{
    int flag = 1;
    // 必须是静态资源
    static Object o1 = new Object();
    static Object o2 = new Object();

    @Override
    public void run() {
        System.out.println("flag= " + flag);
        if(flag == 1){
            synchronized (o1){
                System.out.println(Thread.currentThread().getName() + "我抢到了o1,还需要o2");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("111");
                }
            }
        }
        if(flag == 0){
            synchronized (o2){
                System.out.println(Thread.currentThread().getName() + "我抢到了o2,还需要o1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println("222");
                }
            }
        }
    }
}

public class DeadLock {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        // 设置线程1先抢占o1
        myThread1.flag = 1;
        // 设置线程1先抢占o2
        myThread2.flag = 0;
        Thread t1 = new Thread(myThread1);
        Thread t2 = new Thread(myThread2);
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
有两个线程,都需要锁住同样的2个对象(a、b)才能完成操作,其中线程1已经锁住了a对象,线程2锁住了b对象,两个线程都不释放锁就会造成死锁。程序无法停止。
需要注意的是 o1o2 必须是static的,若不是static则不是共享变量,而是线程各自拥有,则不会有死锁问题。

3.1 死锁检测

一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程查看到底是哪个线程出现了问题。

① Jstack命令

jstack是java虚拟机自带的一种堆栈跟踪工具。
jstack用于打印出Java堆栈信息,生成java虚拟机当前时刻的线程快照。
线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁死循环请求外部资源导致的长时间等待 等。
线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

首先,我们通过jps确定当前执行任务的进程号:

jonny@~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher
jonny@~$ jstack -F 1362
Attaching to process ID 1362, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 23.21-b01
Deadlock Detection:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock Monitor@0x00007fea1900f6b8 (Object@0x00000007efa684c8, a java/lang/Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock Monitor@0x00007fea1900ceb0 (Object@0x00000007efa684d8, a java/lang/Object),
  which is held by "Thread-1"

Found a total of 1 deadlock.

可以看到,进程的确存在死锁,两个线程分别在等待对方持有的Object对象。

② JConsole工具

Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。
它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
在命令行中敲入jconsole命令,会自动弹出以下对话框,选择进程1362,并点击“链接”:
在这里插入图片描述
进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁”:
在这里插入图片描述
在这里插入图片描述
可以看到进程中存在死锁。
以上例子我都是用synchronized关键词实现的死锁,如果读者用ReentrantLock制造一次死锁,再次使用死锁检测工具,也同样能检测到死锁,不过显示的信息将会更加丰富。

3.2 死锁预防

如果一个线程每次只能获得一个锁,那么就不会产生锁顺序的死锁。

先介绍避免死锁的几个常见方法:
(1)避免一个线程同时获取多个锁。上面的例子就是一个线程占用两个锁。
(2)避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
(3)尝试使用定时锁,使用 lock.tryLock(timeout)来替代使用内部锁机制。
(4)对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
(5)线程池死锁 :扩大线程池线程数 or 任务结果之间不再互相依赖。

然后我们再一个一个解释下:
① 若真的需要获得多个锁,那要注意获取的顺序,比如:
在这里插入图片描述
改成:
在这里插入图片描述
那么死锁就永远不会发生。 针对两个特定的锁,开发者可以尝试按照锁对象的 hashCode 值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。

但如果此时有多个线程,都在竞争不同的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就无法满足要求了,这个时候开发者可以使用银行家算法,所有的锁都按照特定的顺序获取,同样可以防止死锁的发生。

②关于第三条的定时锁
当使用 synchronized 关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而 Lock接口 提供了 boolean tryLock(long time, TimeUnit unit) throws InterruptedException 方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
在这里插入图片描述
③ 线程池死锁
线程池的任务1依赖任务2的执行结果,但是线程池是单线程的,也就是说任务1不执行完,任务2永远得不到执行,那么因此造成了死锁。
**加粗样式**
解决办法:扩大线程池线程数 or 任务结果之间不再互相依赖。

小结

死锁就是“两个任务以不合理的顺序互相争夺资源”造成,因此为了规避死锁,应用程序需要妥善处理资源获取的顺序。
另外有些时候,死锁并不会马上在应用程序中体现出来,在通常情况下,都是应用在生产环境运行了一段时间后,才开始慢慢显现出来,在实际测试过程中,由于死锁的隐蔽性,很难在测试过程中及时发现死锁的存在,而且在生产环境中,应用出现了死锁,往往都是在应用状况最糟糕的时候——在高负载情况下。因此,开发者在开发过程中要谨慎分析每个系统资源的使用情况,合理规避死锁。

四、生产者和消费者模式

生产者和消费者模式是用来解决死锁问题的。
为什么可以解决呢?
生产者和消费者模式中,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能,需要根据信号来生产或获取:当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产出来。这里实际体现了信号灯法:flag = true,生产者生产,消费者等待;反之,生产者等待,消费者生产。

下面以蒸包子和吃包子为例,厨师作为生产者输出包子,吃包子的作为消费者,如果没有采用生产者消费者模式,那就可能出现吃包子的没包子吃或者蒸包子的产能过剩的情况;而采用生产者消费者模式后,厨师蒸好包子后不是接着蒸,而是把蒸好的包子放在篮子里然后停下来告诉消费者可以吃了,然后消费者接到信号开始从篮子里拿包子吃,直到吃完后会给厨师一个信号,告诉他可以接着蒸包子了,这时消费者停下,厨师开始蒸包子,接着循环下去。

/**
 * 装包子的篮子类
 */
class Container{
    private String baozi;
    // 篮子状态信号标志:flag = true,篮子空了,生产者生产;flag = false,篮子满了,消费者消费
    private boolean flag = true;

    // 模拟生产过程,生产过程要加锁,防止生产过程中消费者过来消费
    public synchronized void play(String baozi) throws InterruptedException {
        // 生产者等待
        if(!flag){
            this.wait();
        }
        // 生产者生产
        Thread.sleep(500); // 模拟生产耗时
        // 包子蒸好了
        this.baozi = baozi;
        System.out.println("生产" + baozi);
        // 通知消费者来吃
        this.notify();
        // 停止生产
        this.flag = false;
    }

    // 模拟消费过程,消费过程要加锁,防止消费过程中生产者过来生产
    public synchronized void eat() throws InterruptedException {
        // 消费者等待
        if(flag){
            this.wait();
        }
        // 消费者消费
        Thread.sleep(100); // 模拟消费耗时
        System.out.println("篮子里的包子已经吃完了");
        // 通知生产者需要蒸包子了
        this.notify();
        // 停止吃包子
        this.flag = true;
    }
}

/**
 * 定义生产者
 */
class Player implements Runnable{
    private Container container;
    public Player(Container container) {
        this.container = container;
    }

    // 单日生产素馅的包子,双日生产肉馅的包子
    @Override
    public void run() {
        for (int i = 0; i < 6; i++) {
            if(i%2 == 0){
                try {
                    container.play("肉馅的包子");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                try {
                    container.play("素馅的包子");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

/**
 * 定义消费者
 */
class Consumer implements Runnable{
    private Container container;
    public Consumer(Container container) {
        this.container = container;
    }

    // 不管啥馅的包子我都吃
    @Override
    public void run() {
        for (int i = 0; i < 6; i++) {
            try {
                container.eat();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class CpDemo {
    public static void main(String[] args) {
        // 同一个篮子
        Container container = new Container();

        Player player = new Player(container);
        Consumer consumer = new Consumer(container);
        new Thread(player, "生产者").start();
        new Thread(consumer, "消费者").start();
    }
}
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了
生产肉馅的包子
篮子里的包子已经吃完了
生产素馅的包子
篮子里的包子已经吃完了

以上,生产者线程和消费者线程交替进行,就能很好的避免死锁问题。

一般为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:

  1. 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
  2. 如果共享数据区为空的话,阻塞消费者继续消费数据;

因此,生产者消费者模型的作用主要是:

  1. 【运行效率】:通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,不会互相影响抢夺共享资源,这是生产者消费者模型最重要的作用
  2. 【解耦】:解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约,这是生产者消费者模型附带的作用。

在实现生产者消费者可以采用三种方式:
1.使用 Object 的 wait/notify 的消息通知机制;
2.使用 Lock 的 Condition 的 await/signal 的消息通知机制;
3.使用 BlockingQueue 实现。本文主要将这三种实现方式进行总结归纳。
上面的例子用的是第一种方式。

五、volatile和synchronized的区别

volatile关键字主要是在属性定义上使用,表示此属性为直接数据操作,而不进行副本的拷贝处理。

volatile和synchronized的区别:

  • volatile主要是在属性定义上使用,而synchronized是在代码块或方法上使用
  • volatile无法描述同步处理,是一种直接内存处理,避免了副本操作;synchronized是同步操作。

六、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关键字。

七、notify和notifyAll的区别?

先要理解锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

notify和notifyAll的区别

  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
    当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或
  • notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

八、并发编程的挑战

8.1 上下文切换

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。

8.2 资源限制

(1)什么是资源限制
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等。
(2)资源限制引发的问题
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。例如,之前看到一段程序使用多线程在办公网并发地下载和处理数据时,导致CPU利用率达到100%,几个小时都不能运行完成任务,后来修改成单线程,一个小时就执行完成了。
(3)如何解决资源限制的问题
对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
(4)在资源限制情况下进行并发编程
如何在资源限制的情况下,让程序执行得更快呢?方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非

九、并发机制的底层实现原理

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
volatile的实现原理与应用:
在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当
的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
Java语言规范对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存
模型确保所有线程看到这个变量的值是一致的。
有volatile变量修饰的共享变量进行写操作的时候会多出一行Lock前缀的指令,Lock前缀的指令在多核处理器下会引发了两件事情 :

  • 1)将当前处理器缓存行的数据写回到系统内存。
  • 2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
    正常情况下,为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。
    如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

synchronized的实现原理与应用:
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。

  • 对于普通同步方法,锁是当前实例对象。 ·
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。
    当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面会存储什么信息呢?
    JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,使用monitorenter和monitorexit指令实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

ThreadLocal的使用:
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
下面代码中,构建了一个常用的Profiler类,它具有begin()和end()两个方法,而end()方法返回从begin()方法调用开始到end()方法被调用时的时间差,单位是毫秒。
在这里插入图片描述在这里插入图片描述
Profiler可以被复用在方法调用耗时统计的功能上,在方法的入口前执行begin()方法,在方法调用后执行end()方法,好处是两个方法的调用不用在一个方法或者类中,比如在AOP(面向方面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行end()方法,这样依旧可以获得方法的执行耗时。

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

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