购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.3 线程安全

在多线程编程当中,一个不可避免也是最需要关注的问题就是线程安全。与进程拥有独立的进程空间不一样,一个进程的多个线程是共享该进程资源的。由于CPU通常为多核架构,且线程又是并发执行的,所以可能会出现同一时刻存在多个线程同时访问同一个资源的现象。如果在这些并发执行的线程中,有些执行的是修改操作,则可能会因多个线程同时进行修改而导致数据不一致问题,或者由于JVM的内存模型设计是每个线程将共享资源加载到自己的本地内存中,造成某个线程对该共享资源的修改操作,对其他线程不可见,从而出现数据不一致性问题。

为了解决以上线程对共享资源的并发操作而导致的数据不一致性问题,在JDK的设计当中,提供了几种方法来协调多个线程之间对共享资源的访问,从而解决并发访问、修改问题,实现线程安全。以下详细分析这几种方法。

3.3.1 synchronized关键字与互斥锁

与操作系统通过加互斥锁Metux来同步多个线程对共享资源的访问,保证任何时候只有一个线程可以访问共享资源类似,Java也提供了synchronized这个关键字来实现互斥锁,保证多个线程对共享资源的互斥访问。synchronized关键字的作用主要体现在三个方面。

(1)确保线程互斥地访问同步代码;

(2)保证共享变量的线程可见性;

(3)禁止指令重排。

其中(2)和(3)相当于下一小节会介绍的volatile关键字的作用。

1.互斥锁实现线程同步

在使用层面,synchronized关键字可以用在方法或代码块中,其中方法包括类的静态方法和类的成员方法。由于synchronized关键字在实现层面是结合一个监视器对象monitor来实现的,所以在使用的时候需要将某个对象作为监视器对象monitor,具体分析如下。

(1)类的静态方法:将类对象自身(类型为java.lang.Class)作为监视器对象monitor,对该类所有使用了synchronized关键字修饰的静态方法进行同步,即任何时候只能存在一个线程调用该类的使用了synchronized修饰的静态方法,其他调用了该类的使用了synchronized修饰的静态方法的线程需要阻塞,包括该静态方法和其他使用了synchronized关键字来同步的静态方法。

(2)类的成员方法:使用类的对象实例作为监视器对象monitor,则该类所有使用了synchronized关键字修饰的成员方法,在任何时刻只能被一个线程访问,其他线程需要阻塞。

(3)代码块:使用某个对象作为监视器对象monitor,通常为一个普通的private成员变量,如private Object object=new Object(),这样所有使用了该object对象的同步代码块,在任何时候只能存在一个线程访问。

下面简单演示一下synchronized关键字用在类的静态方法、类的成员方法和代码块上。由于这三种情况一般使用不同的对象作为监视器对象monitor,所以不会相互影响。只有使用相同的监视器对象monitor的方法或者代码块才会相互影响,需要互斥访问。代码如下:

2.实现生产者消费者模型

除作为互斥锁对线程进行同步外,synchronized关键字还可以与监视器对象monitor的wait、notify和notifyAll方法一起使用,实现线程之间的协作,即实现线程的生产者和消费者模型。参与的多个线程共享一个监视器对象monitor,在线程持有synchronized同步锁时,才能调用监视器monitor的wait、notify或者notifyAll方法。其中wait方法用于释放监视器对象monitor锁,阻塞休眠,等待其他线程唤醒;notify方法用于通知和唤醒其中一个阻塞休眠的线程,让该线程去获取monitor锁;notifyAll方法用于通知所有阻塞休眠的线程去竞争monitor锁。

synchronized关键字结合监视器对象monitor实现的生产者消费者模型的使用示例如下。生产者线程和消费者线程共享同一个监视器对象monitor,然后通过调用该监视器对象的wait和notify方法来进行相互通知和协作。

当没有产品可消费时,消费者线程调用wait方法等待生产者线程生成产品。之后生产者线程生成产品并放到队列中,并调用notify(或者notifyAll)方法通知和唤醒消费者线程去消费产品。在这个例子中生产者线程每隔5秒生产一个产品,故消费者线程每隔5秒才能消费到一个产品。

(1)生产者消费者模型的字段定义包括监视器对象monitor,存放产品的队列products。具体定义如下:

(2)消费者线程消费产品的方法定义:当等待队列不为空时,则取出队列的产品消费。代码如下:

(3)生产者线程生产产品的方法定义:当队列满时,等待消费者消费产品;当队列不满时,生产产品并填充到队列,同时通知消费者继续消费。代码如下:

(4)main程序启动主方法定义:生产者线程和消费者线程共享同一个生产者消费者模型对象producerConsumer,其中队列设置容量为1。代码如下:

(5)程序执行每隔5秒生产与消费一次,每次都是生产者线程先生产,消费者线程马上消费,消费完进入等待状态。结果如下:

由以上这些代码分析可知,synchronized关键字使用方便,无须在应用代码中显式地执行加锁和解锁操作,只需在对应的方法或者代码块中使用synchronized关键字进行修饰即可。在运行时,由JVM自身实现自动地加锁和解锁操作。

在并发线程同步的性能方面,synchronized关键字修饰的范围越小,线程并发度越高,性能越好。所以通常使用同步代码块,而不是同步方法来缩小同步范围、优化并发性能。

3.实现原理

在JVM层面,synchronized关键字是基于JVM提供的monitorenter和monitorexit字节码指令,以及结合监视器对象monitor来实现的。由上面的分析可知,当synchronized关键字用在类的静态方法、类的成员方法、代码块时,分别需要以类对象自身、类的对象实例本身、某个普通对象作为对应的监视器对象monitor。

结合Java虚拟机JVM的相关知识,任何Java类都需要编译成class字节码,然后加载到Java虚拟机中去执行。在编译一个Java类生成对应class字节码,当遇到synchronized关键字时,会在synchronized关键字所修饰的方法或者代码块的开始处增加一个monitorenter字节码指令,在方法或者代码块的结束处增加monitorexit字节码指令,即使用monitorenter和monitorexit字节码指令包围该方法或者代码块对应的字节码。

下面以在成员方法内部使用类对象实例自身作为监视器对象monitor来分析,这种与在类的成员方法中使用synchronized关键字类似,都是使用类对象实例自身作为监视器对象。代码如下:

反编译该类对应的class字节码文件如下:在成员方法method对应的字节码周围使用了monitorenter和monitorexit字节码指令,具体为3:monitorenter和13:monitorexit,中间主要为System.out.println()方法调用的字节码。代码如下:

(1)monitorenter指令:进入同步块。所有线程共享该同步代码和该对象关联的监视器monitor,当每个线程执行到monitorenter指令的时候,会检查对应的monitor对象的计数是否为0。计数是0,则当前线程成为该monitor对象的拥有者,即锁住了该监视器对象monitor并递增该对象的计数为1。之后该线程每调用一次使用了该monitor对象作为监视器对象的同步方法时,monitor对象的计数加一,这里就是synchronized关键字作为可重入锁的实现。对于其他线程,当检查到monitor对象的计数不为0时,知道该monitor对象已经被其他线程持有锁住了,故这些线程会阻塞。当该monitor的计数重新变为0时,这些阻塞的线程会继续竞争成为该monitor的拥有者,成功的线程可以访问同步代码。

(2)monitorexit指令:退出同步块。当持有该监视器对象monitor的线程每执行完一个同步代码时,会将monitor对象的计数减一。如对于类的对象成员方法,如果该线程调用了多个使用synchronized关键字修饰的成员方法,则每个方法执行完之后,对monitor对象的计数执行一次减一操作。

当monitor对象的计数递减到0时,当前线程不再持有该monitor对象。其他阻塞的线程此时可以竞争成为该monitor的拥有者。

以上介绍了在Java虚拟机层面,基于synchronized关键字实现同步锁的相关原理。在操作系统层面,synchronized关键字是基于操作系统的互斥锁Metux Lock来实现的。不过操作系统实现线程之间的切换是需要进行线程上下文切换的,即从用户态切换到内核态,所以基于synchronized关键字来实现线程同步的成本还是相对较高,性能相对较低。

4.生产者消费者模型实现

关于synchronized关键字的用法部分,我们介绍了使用synchronized关键字和结合监视器对象monitor来实现生产者消费者模型。在生产者消费者模型的实现当中,通过监视器对象monitor的wait方法和notify、notifyAll方法来实现线程之间的协作都需要在synchronized关键字修饰的同步代码内部执行,其主要原因如下。

每个监视器对象monitor都会关联一个等待队列和一个同步队列,其中等待队列用于存放调用了wait方法的线程,同步队列在存放之前则是存放在等待队列中,当被其他线程调用notify或者notifyAll方法唤醒,可以去竞争同步锁时,就被转移到同步队列的线程,即由同步队列的线程去竞争该锁。

所以某个线程在执行wait、notify、notifyAll方法时,需要该线程成为该监视器对象的拥有者,获取到锁才可以访问synchronized关键字包围的同步代码,这样才能有权访问该监视器对象对应的等待队列和同步队列,将线程自身放到该等待队列或者同步队列。

3.3.2 volatile关键字与线程可见性

前面章节介绍了基于synchronized关键字实现的互斥锁来对多个线程进行同步,保证多个线程对共享资源访问的线程安全性。除提供互斥锁实现外,synchronized关键字的另外一个功能是实现共享变量的线程可见性。而在Java语言实现中,还有另一个关键字是专用于实现线程可见性的,这个关键字就是volatile关键字。

1.Java内存模型与线程可见性

在介绍volatile关键字之前,首先介绍一下Java的内存模型(JMM)和线程可见性的含义。Java内存模型是Java虚拟机为了屏蔽各种硬件和底层操作系统的内存访问差异,实现Java应用程序能以统一的方式来访问各种操作系统平台的内存而设计的一种内存访问模型。Java的内存模型的基本组成结构如图3.2所示。

图3.2 Java内存模型

由上图可知,每个线程都拥有自己的工作内存,并且该线程的所有操作都是在这个工作内存中完成的,即每个线程都将主内存中的共享数据复制到自身的工作内存来进行操作,所以不同线程之间的操作是相互不可见的。

知识拓展:可以这样理解Java内存模型,虽然Java内存模型跟操作系统的物理内存和CPU的缓存没有必然的联系,但是也可以结合操作系统的相关知识来理解Java的内存模型。即Java内存模型中线程的工作内存就相当于CPU的本地缓存,主内存就相当于操作系统的物理内存。

如果两个线程需要基于这个共享数据来进行协作,如一个线程类似于赛跑的发令员,另外一个线程是赛跑者,发令员线程修改共享变量的状态为“开始跑”,此时由于发令员是对自身的本地内存的数据状态进行修改,而赛跑者也是只看自己本地内存的数据状态,所以该状态的修改对赛跑者是不可见的,从而导致发令员“发令”后,赛跑者还是停留在原地不动,对于程序来说就是不正常运行了。

所以在多线程环境下,为了保证程序的正常运行,对于共享数据需要实现线程可见性。即任何一个线程对该共享数据进行操作后都需要对其他线程可见,保证其他线程可以读到这个共享数据的最新值,从而可以在这个最新值的基础上继续进行操作,实现数据的一致性。

在Java语言中,要实现线程可见性这个功能要靠volatile关键字。对应到Java内存模型就是当某个线程在自身的本地内存中修改了这个使用volatile修饰的变量时,需要将这个变量的最新值同步回主内存,同时其他线程的本地内存中的该变量的副本会自动失效,从而需要重新从主内存加载这个变量的值,此时读取到的就是该变量的最新值了。

volatile关键字的一个典型应用就是用于修饰boolean类型的控制开关,即该开关刚开始是关闭的,值为false。一个或多个线程阻塞等待这个开关打开,之后另一个线程设置这个控制开关的值为true来打开这个开关。由于使用了volatile关键字修饰这个开关变量,故这个修改对其他阻塞等待的线程是可见的,所以这些阻塞等待的线程可以马上读到值为true,从而继续运行。

控制开关的具体实现,可以定义一个static类型的控制开关onCtrl来控制是否开始工作。定义一个工作线程workThread来执行工作,在workThread线程内调用doWork方法之前,会先在while循环中无限等待直到开关onCtrl打开。最后在主线程中调用turnOn方法来打开这个控制开关。关于volatile关键字的使用的示例代码如下:

首先不使用volatile关键字来修饰onCtrl变量,即打开以上的代码注释,运行程序时,打印“onCtrl is on”后,程序一直阻塞不动。

当使用volatile关键字修饰onCtrl变量之后,重新运行程序,打印如下:

可以看到打印了“doing work now.”,并且程序正常终止。所以doWork方法前面的无限循环退出了,doWork方法被调用了,原因是此时onCtrl变量使用了volatile关键字修饰,工作线程workThread读取到了主线程调用turnOn方法将开关变量onCtrl设置为true这个最新值。

2.不具备原子性

在线程可见性方面,volatile关键字相对于synchronized关键字是一种更加轻量级的实现,所以在功能上,volatile关键字也没有synchronized关键字那么强大。在进行Java程序设计时,需要重点关注的是volatile关键字并不提供原子性,即对于共享变量的复合操作,volatile并不能保证整个操作的原子性和线程安全。一个典型的例子就是整数的自增操作,即“++”操作。自增操作由读取、递增一和赋值这三个操作组成。

例如,定义20个线程,每个线程调用“++”递增操作累加10万次,正常来说最后sum的值应为20×100,000=2000,000,即200万。volatile关键字不具备原子性的示例代码如下:

首先累加和变量sum使用基本类型int,且使用volatile关键字修饰,程序执行打印如下:

可以看到值并不是200万,计算错误。所以即便使用了volatile关键字修饰,由于volatile关键字不具备原子性,因此也会出现由于线程的并发修改导致的数据不一致性。

如果想要得到正确的结果,一般会使用Java并发包的线程安全的原子类来实现。如下为使用整数原子类AtomicInteger来定义累加和sum(具体可以打开以上代码的注释来演示),对应的执行打印结果为正确打印200万。

3.禁止指令重排

除了保证线程可见性之外,volatile关键字还有一个重要的功能就是禁止指令重排。为了提高代码的执行性能,Java编译器在编译Java代码时,或者CPU在执行指令时,会进行一个指令重排操作。具体为在不影响单线程执行的前提下,调整没有依赖关系的指令位置,从而使得CPU能够更高效地执行这些指令。

指令重排在单线程执行中没有问题,但是在多线程环境中则可能由于指令顺序的前后调整而导致执行出错。一个典型的例子如下:

核心代码实现为:

(1)定义变量sum并初始为1。

(2)在主方法中,创建一个子线程thread,在内部等待isUpdate变为true,然后执行doWork方法。

(3)在主线程调用updateSum方法,更新sum的值为2,同时设置isUpdate为true。

分析:由于sum和isUpdate没有依赖关系,故在updateSum方法中可能会发生指令重排,即sum=2和isUpdate=true这两个操作反过来。

如果反过来之后,在执行了isUpdate=true之后,还没执行sum=2之前,子线程由于isUpdate变为true而退出等待,继续执行sum+=1操作,此时sum由于还是1,故结果为2,而不是预期的3。如果以上代码对isUpdate使用volatile关键字修饰,则不会发生指令重排,即在updateSum方法中,严格按照先执行sum=2,再执行isUpdate=true的步骤来执行。

所以如果代码中可能存在这种由于指令重排而导致的错误,则可以使用volatile关键字对相应的变量进行修饰。此时Java编译器在编译这段Java代码或者CPU在执行这段代码时,不会对该变量的位置与其相邻的其他变量进行调整。

在禁止指令重排的实现层面,主要是通过在使用了volatile关键字修饰的变量周围加上内存屏障指令来实现的。关于JVM内存屏障的相关指令这里就不继续展开了,有兴趣的读者可以查阅相关资料进一步了解。

3.3.3 不可变的final关键字与无状态

1.基于final关键字实现对象的不可变

在Java多线程编程当中存在线程安全问题,主要是因为多个线程共享了同一个资源,如同一个对象实例,并且每个线程都可能对这个对象实例进行修改操作,所以在多线程并发修改时,可能会出现数据不一致性。不过,如果多个线程只是对共享资源进行读操作,而不会进行修改等写操作,则不会存在线程安全问题的,即不通过额外的加锁操作,多个线程也是可以访问共享变量的。

基于这种背景,如果被多个线程共享的对象是只读、不可变的,则可以使用Java的final关键字进行修饰。使用final关键字修饰某个对象之后,就不能对这个对象进行修改操作了,这个对象也是线程安全的。此时对该对象的访问无须通过synchronized关键字来加锁。

在具体使用层面,当使用final关键字修饰某个对象时,分为以下几种情况。

(1)对于基本类型的包装类型,如int、long等对应的Integer、Long类型对象,不可变的语义是其数值不能修改。

(2)如果是其他对象,如我们自定义的Java对象或者Java语言自身的其他对象类型,则该对象引用不能指向另外一个该类型的其他对象,如final Object a=new Object(),此时不能执行a=b这种操作。但是该对象内部的属性值需要根据属性是否使用final关键字修饰来决定是否可以修改。

(3)除了显式使用final关键字来修饰某个对象达到不可变之外,Java语言自身的实现也保证了某些对象类型是不可变的。一个典型的类型就是字符串类型String。字符串String类型的值是不可以修改的,即不能单独修改该字符串的某个字符。如果需要修改,则只能将该String对象引用指向另一个完整的字符串。例如,str是可以正常指向新的字符串“world”的,而str2是不可以重新指向“change”的,因为str2使用了final关键字修饰。所以String类型的对象是线程安全的。代码如下:

final关键字用于修饰业务中不可变的对象,这样在多线程中可以放心使用这个对象,而不需要显式加锁操作。不过前提是对象本身确实是不可变的才能使用final关键字修饰。如果在业务上,对象本身是可变的,那么final关键字就无能为力了,只能通过加锁来实现同步访问。而加锁操作由于会涉及底层操作系统的线程上下文切换,因而开销较大,性能较低。

所以为了应对对象是可变的且加锁性能低下的问题,在Java多线程编程当中,还可以基于无状态设计来实现线程安全。

2.无状态设计

在讲解无状态设计之前,首先来介绍一下线程与方法调用之间的关系。

在Java虚拟机的运行时数据区中,通常包含堆、栈和方法区,其中栈是用于存放线程进行方法调用时的相关数据的,包括该方法的方法参数、局部变量、返回值地址等。而在栈中,每个线程都对应一个独立的栈帧,即该栈帧是每个线程的私有数据区域,其他线程不能访问。当线程每进行一次方法调用时,就会将该方法的参数、局部变量、返回值地址等数据入栈。在方法调用完成之后,进行以上数据的出栈。

由于栈帧是线程私有的区域,不会被其他线程访问,故不存在多线程并发导致的线程安全问题。与此同时,由于任何时候一个线程只能调用一个方法,所以能够保证方法的内容得到顺序执行。以上介绍的线程的方法调用与栈的对应关系如图3.3所示。

图3.3 线程方法调用与栈的关系

所以当某个对象被多个线程共享且有多个线程同时调用该对象的某个方法时,如果该方法只包含(1)方法调用时传递进来的参数数据,(2)方法内部定义的局部变量,而不会使用到该对象的成员变量(成员变量也是被多个线程共享的),则该方法是无状态的方法,是线程安全的。因为该方法是基于方法参数和局部变量来进行操作的,而这些数据是线程的栈帧的私有数据,故不会被其他线程访问,不存在线程安全问题。

无状态设计一个最典型的应用就是结合Spring框架来进行Java Web应用开发。在应用中定义的请求处理器Controller、服务类Service等都是单例的,这些单例对象被服务端的请求处理线程所共享,如被基于Tomcat部署的Java Web应用使用的多个Tomcat工作线程所共享。这些单例类的方法是线程安全的,即不同客户端请求之间不会相互影响,而其中的实现原理就是无状态。

无状态设计体现在Controller和Service类对象实例的方法都是只包含方法参数数据和内部的局部变量,一般不会使用到对象成员变量(注意这里所指的对象成员变量不包括通过注入,如通过@Autowired注解注入的其他bean对象,如果是注入的bean,其本身也是线程安全的)。所以每个客户端请求都是基于该客户端提供的请求处理方法参数数据来进行相关处理的,不同请求的客户端数据不会相互影响。

如下是一个用户控制器UserController的登录方法login的实现,方法参数为当前登录用户。由于是每个服务器处理线程在每次调用这个方法时都是传递独立的用户参数,所以服务器处理线程之间不会相互影响。并且该方法内部也只包含局部变量,所以是一个无状态的方法。代码如下:

不过,如果在Controller、Service等类的方法需要访问该类对象实例的成员变量时,该成员变量就需要保证线程安全,因为这些成员变量被所有服务端处理线程共享,并不是无状态的。具体可以使用Java并发包的线程安全类,如线程安全的ConcurrentHashMap来替代HashMap、原子整数类型AtomicInteger来替代Integer等,具体例子如下。

如下是一个消息发送控制器MessagePublishController,为了统计当前服务实例所发布的消息的总条数,在MessagePublishController类内定义一个Long类型的成员变量broadCastCount。由于MessagePublishController的对象实例是单例的,所以broadCastCount也是被所有服务端处理线程所共享的。同时由于broadCastCount用于统计当前服务实例所发布的消息的总条数,所以是有状态的,故需要使用Long类型的原子类型AtomicLong,并调用AtomicLong的incrementAndGet方法来完成递增。具体代码定义如下:

3.3.4 ThreadLocal线程本地变量

对于被多个线程共享的变量,一般的方式是通过加锁,如使用synchronized关键字或者Java并发包的ReentrantLock(后面会详细介绍)来实现线程安全。或者如果该变量对应的类型在Java并发包存在线程安全的实现版本,如整数Integer对应的AtomicInteger、HashMap对应的ConcurrentHashMap等,则使用对应的线程安全的实现版本。除此之外,通过无状态设计也可以实现多线程并发访问的线程安全,这样就不用加锁或者使用Java并发包的线程安全类,不过这种需要业务场景本身就是无状态。

当需要使用到有状态的共享变量时,除通过加锁、使用线程安全版本的数据类型这些方式来实现有状态变量的线程安全之外,还可以使用Java提供的一个特殊的类,这个类就是ThreadLocal,线程本地变量包装器。

ThreadLocal是基于空间换时间的思路来设计的,即通过使用ThreadLocal对共享变量进行包装,使得每个线程都包含这个共享变量的一个副本,每个线程都对自己的共享变量副本进行操作,这样就实现了这个共享变量对每个线程的独立性,不需要通过加锁来实现线程安全。

注意

不要与线程可见性的线程本地内存弄混,ThreadLocal包装的共享变量是不需要与其他线程共享和进行协作的,本身就是要实现数据对其他线程不可见。

不过由于每个线程都包含了这个共享变量的一个副本,所以会额外占用一定的内存空间,并且会随着线程数量的增加而增大,特别是如果这个共享变量会占用比较大空间时,如用于存放数据的字典结构HashMap,则空间会增大更多。所以在选择是否使用ThreadLocal时,需要对该共享变量的空间占用进行衡量,如果是简单的数据类型,如整数,则可以使用,否则需要考虑内存占用问题。

1.用法

在使用层面,类的静态变量或者被多个线程共享的对象实例的内部属性,一般可以通过ThreadLocal进行包装来实现线程的独立性和线程安全。

例如,定义一个被所有线程共享的、类型为Integer的操作计数器counter,该计数器主要用于记录每个线程自身完成了多少次操作。

由于每个线程都不一样,而该操作计数器又是被所有线程所共享,所以可以使用ThreadLocal对其进行包装来实现每个线程都是对自身的计数器副本进行递增,不同线程不会相互影响。具体的代码如下:

打印如下:

分析:线程1递增了100万次,线程2递增了200万次。即使计数器使用了存在线程安全问题的Integer类型,并且使用复合操作“++”进行递增,由于使用了ThreadLocal进行包装,使得两个线程都是对自身的操作计数器线程副本进行操作,所以不会影响其他线程,不存在竞争问题。

2.核心实现原理

在ThreadLocal的实现层面,首先是在线程类Thread内部会包含一个字典类型的成员变量threadLocals,定义如下:

可以看出类型是ThreadLocal.ThreadLocalMap。threadLocals是用于存放该Thread线程对象所用到的,所有通过ThreadLocal包装的变量的集合。

其中这个字典类型是在ThreadLocal内部定义的一个静态内部类ThreadLocalMap, ThreadLocalMap内部字典结构实现是,key是ThreadLocal对象引用,值为该ThreadLocal对象所包装的具体值。由于每个Thread线程对象都包含这样一个字典集合,所以实现了每个线程都包含共享变量的一份副本。

由以上分析可知,Thread类的线程本地变量字典threadLocals的类型ThreadLocalMap是在ThreadLocal中定义的,ThreadLocalMap的核心定义如下:

分析:可以看出与常用的字典结构HashMap类似,也是基于链式哈希表实现的。

(1)ThreadLocal的值初始化。

当使用ThreadLocal对某个变量进行包装时,首先需要对这个变量进行初始化,不过也可以通过调用set方法在之后使用时再设值。ThreadLocal变量的初始化主要是通过其initialValue方法来实现的,具体如下:默认实现为返回null,该方法是protected方法,故可以在创建ThreadLocal对象时,重写这个方法来自定义初始化逻辑。

重写initialValue方法来自定义初始化逻辑,初始化Integer类型的线程操作计数器的值为0的代码如下:

(2)ThreadLocal的get方法:获取线程绑定的值。

初始化值或者调用set方法写值之后,在使用时一般会通过ThreadLocal的get方法来获取该ThreadLocal所包装的变量对应的值。

由于每个线程都是获取到与该线程绑定的值,即从该Thread线程对象所关联的线程本地变量集合threadLocals中获取,所以在get方法的内部实现当中,首先需要获取当前调用这个get方法的线程的Thread对象引用,具体为通过调用Thread.currentThread()方法来获取。然后使用当前的ThreadLocal对象引用作为key,从该Thread线程对象的成员变量threadLocals中获取对应的值,具体实现如下:

(3)ThreadLocal的set方法:设置线程绑定的值。

set方法主要是往Thread线程对象的threadLocals集合中设置该ThreadLocal对应的值。set方法的实现与get方法的实现类似,也是先拿到当前调用这个set方法的线程的Thread对象引用,然后再往该Thread对象引用的threadLocals集合中设置值。其中key为当前的ThreadLocal对象引用,值为通过方法参数传递进来的实际的值,具体实现如下: WteSi+VN2ekUb1Vik6QfoBjPvd2eNIi8BLMC7WusCrWq0lWEaO7n+5yBON+PVhnC

点击中间区域
呼出菜单
上一章
目录
下一章
×