生产版本的应用场景

qq在线状态2022-07-06  23

我们在学习一个工具之前,首先要了解这个工具的功能,能带来什么好处,而不是一上来就去钻研这个工具的API和使用方法。否则,即使我们学会了某个工具的用法,也不知道该在什么场景下使用。所以,我们先来看看哪些场景需要ThreadLocal。

在通常的业务开发中,ThreadLocal有两种典型的使用场景。

场景1,ThreadLocal用于保存每个线程的独占对象,为每个线程创建一个副本,使得每个线程可以修改自己的副本而不影响其他线程的副本,从而保证线程安全。

2.ThreadLocal作为一个场景,信息需要在每个线程中独立保存,以便其他方法可以更方便地获取信息。每个线程获得的信息可能不同。信息被之前执行的方法保存后,后续的方法就可以被ThreadLocal直接获取,这样就避免了参数的传递,类似于全局变量的概念。

典型场景1

这种场景通常用于保存带有不安全线程的工具类,需要使用的典型类是SimpleDateFormat。

风景介绍

在这种情况下,每个线程都有自己的实例副本,这个副本只能被当前线程访问和使用,相当于每个线程内部的局部变量,这也是ThreadLocal命名的意义。因为每个线程都有自己的副本,而不是自己的副本,所以不存在多线程之间共享的问题。

我们打个比方。比如一家餐厅要做一道菜,但是五个厨师在一起做。这就乱套了,因为如果一个厨师已经放了盐,其他厨师不知道的话,都放了一次盐,导致最后一道菜很咸。这就像多线程的情况,线程是不安全的。我们用了ThreadLocal之后,相当于每个厨师只负责一道菜,总共有五道菜,这样就很清楚了,不会有问题。

简单日期格式的演变

1.两个线程都需要使用SimpleDateFormat。

让我们用一个案例来说明这种典型的第一种情况。假设要求两个线程都使用SimpleDateFormat。代码如下:

公共类threadlocaldemo 01 { public static void main(String[]args)抛出interrupted exception { new Thread(-{ String date = new threadlocaldemo 01 . date(1));System.out.println(日期);}).开始;thread . sleep(100);new Thread(-{ String date = new threadlocaldemo 01 . date(2);System.out.println(日期);}).开始;} public String Date(int seconds){ Date Date = new Date(1000 *秒);simple date format simple date format = new simple date format( # 34;mm:ss # 34;);返回simpleDateFormat.format(日期);}}从上面的代码可以看出,两个线程创建了自己的SimpleDateFormat对象,如图:

这样就有两个线程,所以有两个SimpleDateFormat对象,互不干扰。这段代码可以正常工作,运行结果是:

00:01 00:022.10个线程需要使用SimpleDateFormat。

假设我们的需求升级了,需要的不是2个线程,而是10个线程,也就是10个线程同时对应10个SimpleDateFormat对象。让我们看看下面的文字:

公共类threadlocaldemo 02 { public static void main(String[]args)抛出interrupted exception { for(int I = 0;i 10i++){ int finalI = I;new Thread(-{ String date = new threadlocaldemo 02 . date(finalI));System.out.println(日期);}).开始;thread . sleep(100);} }公共字符串date(int秒){ Date date = new Date(1000 *秒);simple date format simple date format = new simple date format( # 34;mm:ss # 34;);返回simpleDateFormat.format(日期);}}上面的代码利用for循环来满足这一要求。for循环总共循环10次,每次创建一个新线程,每个线程在date方法中创建一个SimpleDateFormat对象,如下图所示:

可以看到有10个线程,对应10个SimpleDateFormat对象。

代码的运行结果:

00: 0000: 0100: 0200: 0300: 0400: 0500: 0600: 0700: 0800: 093.需求变成了1000个线程应该使用SimpleDateFormat。

但是线程不能无休止地创建,因为线程越多,占用的资源就越多。假设我们需要1000个任务,就不能再使用for循环的方法,而应该使用线程池来实现线程的重用,否则会消耗过多的内存等资源。

在这种情况下,我们给出以下代码实现方案:

public class threadlocaldemo 03 { public static ExecutorService thread pool = executors . newfixedthreadpool(16);公共静态void main(String[] args)引发interrupted exception { for(int I = 0;i 1000i++){ int finalI = I;thread pool . submit(new Runnable { @ Override public void run { String date = new threadlocaldemo 03 . date(finalI));System.out.println(日期);} });} threadPool.shutdown} public String Date(int seconds){ Date Date = new Date(1000 *秒);simple date format date format = new simple date format( # 34;mm:ss # 34;);return dateFormat.format(日期);}}你可以看到,我们使用了一个16线程的线程池,向这个线程池提交了1000个任务。在每个任务中,它都做与以前相同的事情,或者执行date方法并在该方法中创建simpleDateFormat对象。程序运行结果之一是(多线程下,运行结果不唯一):

00:0000: 0700: 0400: 02 ...16: 2916: 2816: 2716: 2616:39程序运行正确,从00: 00到16:39打印出1000次,没有重复时间。让我们用图形来表示这段代码,如图所示:

左边是线程池,右边是1000个任务。我们刚才做的就是为每个任务创建一个simpleDateFormat对象,也就是1000个任务对应1000个simpleDateFormat对象。

但也没必要这么做,因为创建这么多对象是有成本的,使用后的销毁也是有成本的,同时这么多对象存在内存中也是对内存的浪费。

现在来优化一下吧。既然不想要那么多simpleDateFormat对象,最简单的就是只用一个。

4.所有线程共享一个simpleDateFormat对象

我们使用以下代码来演示仅使用一个simpleDateFormat对象的情况:

public class threadlocaldemo 04 { public static ExecutorService thread pool = executors . newfixedthreadpool(16);静态简单日期格式日期格式=新的简单日期格式( # 34;mm:ss # 34;);公共静态void main(String[] args)引发interrupted exception { for(int I = 0;i 1000i++){ int finalI = I;thread pool . submit(new Runnable { @ Override public void run { String date = new threadlocaldemo 04 . date(finalI));System.out.println(日期);} });} threadPool.shutdown} public String Date(int seconds){ Date Date = new Date(1000 *秒);return dateFormat.format(日期);}}在代码中可以看到,其他的都没有变。所改变的是,我们提取了这个simpleDateFormat对象,并将其转换为一个静态变量。当我们需要使用它时,我们可以直接获取这个静态对象。好像省略了创建1000个simpleDateFormat对象的开销,好像也没什么问题。让我们用图表展示一下:

从图中可以看出,我们有不同的线程,线程将执行它们的任务。但是不同任务调用的simpleDateFormat对象都是一样的,所以指向的对象都是一样的,但是这样就会出现线程不安全的情况。

5.该线程不安全,并且存在并发安全问题。

控制台将打印出(在多线程下,运行结果不是唯一的):

00: 0400: 0400: 0500: 04 ...16: 1516: 1416: 13当您执行上述代码时,您会发现控制台打印的内容与我们预期的不一致。我们期待的是打印的时间没有重复,但是可以看出这里有重复。比如第一行和第二行都是04秒,说明里面出了差错。

锁定

错误的原因是simpleDateFormat对象本身不是线程安全的对象,不应该被多个线程同时访问。于是我们想到了一个解决方案,用synchronized来锁。因此代码被修改成如下所示:

public class threadlocaldemo 05 { public static ExecutorService thread pool = executors . newfixedthreadpool(16);静态简单日期格式日期格式=新的简单日期格式( # 34;mm:ss # 34;);公共静态void main(String[] args)引发interrupted exception { for(int I = 0;i 1000i++){ int finalI = I;thread pool . submit(new Runnable { @ Override public void run { String date = new threadlocaldemo 05 . date(finalI));System.out.println(日期);} });} threadPool.shutdown} public String Date(int seconds){ Date Date = new Date(1000 *秒);字符串s = nullsynchronized(threadlocaldemo 05 . class){ s = date format . format(date);} return s;}}可以看到在date方法中加入了synchronized关键字,锁定了simpleDateFormat的调用。

运行这段代码的结果(多线程下,运行结果不唯一):

00: 0000: 0100: 06 ...15: 5616: 3716: 36这个结果很正常,没有时间重复。但是由于我们使用了synchronized关键字,会陷入排队状态,多个线程无法同时工作,从而整体效率大打折扣。有没有更好的解决办法?

我们希望达到不浪费太多内存的效果,同时又想保证线程安全。经过思考得出结论,每个线程都可以有自己的simpleDateFormat对象来实现这个目标,这样就可以两全其美了。

7.使用线程本地

那么,为了达到这个目的,我们可以使用ThreadLocal。代码如下:

public class threadlocaldemo 06 { public static ExecutorService thread pool = executors . newfixedthreadpool(16);公共静态void main(String[] args)引发interrupted exception { for(int I = 0;i 1000i++){ int finalI = I;thread pool . submit(new Runnable { @ Override public void run { String date = new threadlocaldemo 06 . date(finalI));System.out.println(日期);} });} threadPool.shutdown} public String Date(int seconds){ Date Date = new Date(1000 *秒);simple date format date format = thread safe formatter . dateformatthreadlocal . get;return dateFormat.format(日期);} } class ThreadSafeFormatter { public static ThreadLocalSimpleDateFormat dateformat date local = new ThreadLocalSimpleDateFormat { @ Override protected Sim pleDateFormat initial value { return new SimpleDateFormat( # 34;mm:ss # 34;);} };}在这段代码中,我们使用ThreadLocal来帮助每个线程生成自己的simpleDateFormat对象,该对象是每个线程独有的。但同时这个对象也不会创建太多,总共只有16个,因为只有16个线程。

代码运行结果(多线程下,运行结果不唯一):

00: 0500: 0400: 01 ...16: 3716: 3616: 32这个结果是正确的,不会有重复的时间。

让我们用图表来看看当前的状态:

在图的左侧,可以看到这个线程池中有16个线程,对应16个simpleDateFormat对象。在这张图片的右边,有1000个任务。任务很多,和以前一样有1000个任务。但这里最大的变化是,虽然有1000个任务,但我们不再需要创建1000个simpleDateFormat对象。无论有多少个任务,最终都只会有相同线程数的simpleDateFormat对象。这样既高效的使用了内存,又保证了线程安全。

以上是适合使用ThreadLocal的第一个典型场景。

典型场景2

每个线程都需要存储类似全局变量的信息(比如拦截器中获取的用户信息),这些信息可以被不同的方法直接使用,避免了参数传递的麻烦,但又不想被多个线程共享(因为不同的线程获取的用户信息不同)。

比如ThreadLocal用来存储一些业务内容(用户权限信息,从用户系统获取的用户名,用户ID等。),在同一个线程中是相同的,但是不同的线程使用不同的业务内容。

在一个线程的生命周期中,这个静态ThreadLocal实例的get方法获取它所设置的对象,从而避免了将这个对象(比如用户对象)作为参数传递的麻烦。

让我们以图片的形式举个例子:

比如我们是一个用户系统。假设不使用ThreadLocal,当一个请求进来时,一个线程会负责执行请求,然后请求会依次调用service-1、service-2、service-3、service-4。这四种方法可能分布在不同的类中。

在service-1中,它将创建一个用户对象,用于存储用户的用户名等信息。稍后,service-2/3/4将需要该对象的信息。例如,服务-2代表下订单,服务-3代表发货,服务-4代表关闭订单。在这种情况下,每个方法都需要用户信息,所以需要将这个用户对象逐层传递,从service-1传递到service-2,从service-2传递到service-3,以此类推。

这将导致代码冗余。有什么办法解决这个问题吗?我们想到的第一种方法是使用HashMap,如下图所示:

比如用了这样的地图之后,我们就不需要一层一层的传递用户对象了。而是在执行service-1的时候,把这个用户信息放进去,然后以后需要获取用户信息的时候,可以直接从静态用户图中获取。这样,无论执行哪种方法,都可以直接获得用户信息。当然,我们也应该考虑到web服务器通常是多线程的。当多个线程同时工作时,我们还需要保证线程安全。

所以在这里,如果我们因为HashMap是线程不安全的而使用它还不够,那么我们可以使用synchronized,或者直接用ConcurrentHashMap替换HashMap,以类似的方式保证线程安全。下图显示了这种改进:

在这个图中,我们可以看到有两个线程,每个线程所做的是访问service-1/2/3/4。然后当它们同时运行时,它们将同时访问这个用户映射,所以用户映射必须是线程安全的。

无论我们使用synchronized还是ConcurrentHashMap,都会对性能产生影响,因为即使我们使用性能更好的ConcurrentHashMap,它仍然包含了少量的同步或cas等进程。与完全不同步相比,它仍然遭受性能损失。所以这里更好的方法是使用ThreadLocal。

这样,我们就可以在不影响性能或者不需要逐层传递参数的情况下,保存当前线程对应的用户信息。如下图所示:

如图所示,多个线程同时执行,但是这些线程同时访问这个ThreadLocal,并且可以使用ThreadLocal来获得它们自己的独占对象。这样,就不需要任何额外的措施来确保线程安全,因为每个线程都是用户对象独占的。代码如下:

公共类threadlocaldemo 07 { public static void main(String[]args){ new service 1 . service 1;} }类service 1 { public void service 1 { User User = new User( # 34;拉勾教育 # 34;);UserContextHolder.holder.set(用户);新的service 2 . service 2;} }类service 2 { public void service 2 { User User = usercontextholder . holder . get;system . out . println( # 34;Service2获取用户名: # 34;+user . name);新服务3 . service 3;} }类service 3 { public void service 3 { User User = usercontextholder . holder . get;system . out . println( # 34;3 Service3获取用户名: # 34;+user . name);usercontextholder . holder . remove;} } class UserContextHolder { public static thread local user holder = new thread local;}类用户{字符串名称;用户(字符串名称){this。name = n}}在这段代码中,我们可以看到我们有一个UserContextHolder,它保存了一个ThreadLocal。在调用Service1的方法时,用户对象存储在其中,以后调用时,可以通过get方法直接取出。没有层层传递参数的过程,非常优雅方便。

代码运行结果:

服务2获取用户名:拉勾教育服务3获取用户名:拉勾教育汇总

我们总结一下。

本文主要介绍ThreadLocal的两个典型使用场景。

场景1,ThreadLocal用于保存每个线程的独占对象,为每个线程创建一个副本。每个线程只能修改自己的副本,而不会影响到其他线程的副本,从而使线程在并发情况下原本不安全的情况变成了线程安全的情况。

2.ThreadLocal作为每个线程中需要独立保存信息的场景,以便其他方法更方便地获取信息。每个线程获得的信息可能不同。在前一个方法设置好信息后,后续的方法可以通过ThreadLocal直接获取,避免了参数传递。

转载请注明原文地址:https://juke.outofmemory.cn/read/614347.html

最新回复(0)