写点什么

我熬夜撸完了这篇万字 Java 线程笔记,助我轻松拿到 30k 的字节 offer

发布于: 3 小时前
我熬夜撸完了这篇万字Java线程笔记,助我轻松拿到30k的字节offer

今日分享开始啦,请大家多多指教~

Java 是一个支持多线程语言,线程是比进程更轻量的调度执行单位,线程的引入,将进程的资源调度和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。

实现线程包括 3 种方式:内核线程实现、用户线程实现、用户线程加轻量级进程混合实现。

我们可以将线程和《操作系统》中进程来对比学习,如进程有内核态、用户态。

Java 线程的底层实现原理

内核级线程实现(KLT,Kernel-Level Thread)

内核线程,即 KLT,全称 Kernel-Level Thread,有操作系统内核支持的线程,这种线程(内核级线程)有内核完成线程切换,而内核中又通过操纵调度器(scheduler)对线程调度,并负责将线程的任务映射到各个处理器上。每个内核级线程可以视为内核的一个分量。

由于内核线程的实现,每一个轻量级进程成为一个独立的调度单位,即使是一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作,但是轻量级进程有它的局限性:第一,由于是基于内核线程实现的,所以各种线程操作,如创建、析构与同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态和核心态中来回切换。

第二,每一个轻量级进程都需要一个内核级线程的支持,因此轻量级进程需要消耗一个内核资源,因此一个系统支持轻量级进程的数量是有限的。

用户级线程实现(UT,User Thread)

用户级线程,UT,英文全称 User Thread,存在两个定义方式。

广义的用户线程:不属于内核级线程的线程都是用户级线程,所以,上图中的轻量级进程也属于用户线程。

狭义的用户线程:是指完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核协助(这是重点,记住)。用户线程也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间 1:N 的关系称为一对多的线程模型,如图:

使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。 线程的创建、切换和调度都是需要考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至不可能完成。

以 Java 语言为例,曾经使用过用户级线程,后来又放弃了,现在 Java 使用的是用户线程加轻量级进程的混合模式,且看下面。

注意,现在所使用的 Java 并未使用用户线程实现,使用的是用户线程加轻量级进程混合实现。

用户线程加轻量级进程混合实现

含义:用户线程加轻量级进程混合实现,是一种将内核线程与用户线程一起使用的实现方式。

在用户线程加轻量级进程混合实现下,既存在用户线程,也存在轻量级进程。两者(用户线程和轻量级进程)一起发挥自己的作用:

用户线程的作用:完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。

轻量级进程的作用:由操作系统提供,作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。

在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为 N:M 的关系,如图:

Java 线程调度底层原理(调度方式与线程优先级)

Java 线程调度方式(协同式调度+抢占式调度)

Java 线程调度方式主要两种,分别是协同式调度(Cooperative Threads-Scheduling)和抢占式调度(Preemptive Threads-Scheduling)。

协同式调度: 线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。优点是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。缺点:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。

抢占式调度:线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在 Java 中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java 使用的线程调度方式就是抢占式调度(记住,Java 的抢占式调度就是对同步锁的抢夺)。

线程优先级读写

Java 语言一共设置了 10 个级别的线程优先级(从 1 到 10,1 为优先级最低,10 为优先级最高,Thread.MIN_PRIORITY 表示优先级为 1,Thread.MAX_PRIORITY 表示优先级为 10),在两个线程同时处于 Ready 状态时,优先级越高的线程越容易被系统选择执行。Java 的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统,即系统线程优先级跟 Java 线程的优先级一般对不上。

注意 1:值得注意的是,线程优先级并不是指线程执行的先后顺序,而是线程被执行的概率权重。事实上,除非程序员使用标志位做线程通信,否则 Java 并没有提供任何线程执行先后顺序的机制,哪个线程先执行只取决于 CPU 调度。

注意 2:此外,不同的操作系统支持的线程优先级不同的,建议使用上述三个优先级 MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY,不要自定义。

Java 线程状态转换底层原理(6 种状态)

先介绍线程包含的 6 种状态,然后分别介绍五种状态含义,最后给出状态转换图。

线程状态:新建状态 New、可运行状态 Runnable、等待状态 Waiting、限时等待状态 Timed_Waiting、阻塞状态 Blocked、结束状态 Terminated,在任意一个时间点,一个线程只能有且只有其中的一种状态 。

新建状态 New:创建后尚未启动的线程处于这种状态。

可运行状态 Runnable:Runable 包括了操作系统线程状态中的 Running 和 Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着 CPU 为它分配执行时间。

等待状态 Waiting:处于这种状态的线程不会被分配 CPU 执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:没有设置 Timeout 参数的 Object.wait()方法、没有设置 Timeout 参数的 Thread.join()方法、LockSupport.park()方法。

计时等待状态 Timed_Waiting:处于这种状态的线程也不会被分配 CPU 执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态: Thread.sleep()方法、设置了 Timeout 参数的 Object.wait()方法、设置了 Timeout 参数的 Thread.join()方法、LockSupport.parkNanos()方法、LockSupport.parkUntil()方法。

阻塞状态 Blocked:线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。 在程序等待进入同步区域的时候,线程将进入这种状态。

结束状态 Terminate:已终止线程的线程状态,线程已经结束执行。

线程的状态有不同说法:

有的说 Java 线程 5 种状态,这是因为将“等待状态 Waiting+限时等待状态 Timed_Waiting”作为一种状态,5 种状态为:

新建状态 New、可运行状态 Runnable(Running+Ready)、等待状态 Waiting+Timed_Waiting、阻塞状态 Blocked、结束状态 Terminated

有的说 Java 线程 6 种状态,这是因为将将 Running 和 Ready 两种状态拆分开了,6 种状态为:

新建状态 New、Ready 准备状态、Running 运行状态、等待状态 Waiting+Timed_Waiting、阻塞状态 Blocked、结束状态 Terminated

或者如本文等待状态 Waiting 和限时等待状态 Timed_Waiting 两种状态拆开,6 种状态为:

新建状态 New、可运行状态 Runnable(Running+Ready)、等待状态 Waiting、计时等待状态 Timed_Waiting、阻塞状态 Blocked、结束状态 Terminated

有的说 Java 线程 7 种状态,这是因为将将 Running 和 Ready 两种状态拆分开、等待状态 Waiting 和限时等待状态 Timed_Waiting 两种状态拆开,7 种状态为:

新建状态 New、Ready 准备状态、Running 运行状态、 等待状态 Waiting、计时等待状态 Timed_Waiting、阻塞状态 Blocked、结束状态 Terminated

不管采用哪种说法,Java 线程状态以下几种,新建状态 New、可运行状态 Runnable(Running+Ready)、等待状态(等待状态 Waiting+限时等待状态 Timed_Waiting)、阻塞状态 Blocked、结束状态 Terminated

对于上图(Java 线程状态转换),注意以下四点:

注意 1:New 状态这里是单向箭头,表示只能从 New 状态到 Runnable 状态、不能从 Runnable 状态到 New 状态,即线程新建启动就不能再回来。

注意 2:Terminate 状态这里是单向箭头,表示进入到 Terminate 状态就不能再回去了,表示线程死亡后就不能再复活。

注意 3:只有 New 状态和 Terminate 状态这里是单向箭头,其他都是双向箭头,表示其他状态之间可以相互转换,同时表示任何一个线程一定会经历 New – Runnable – Terminate 这个顺序状态,这三个状态是必备的,其他状态是可选的。

注意 4:很多博客的图中间这个状态都是 Running,笔者任何不合适,笔者这里使用 Runnable (Runnable = Ready + Running).

线程安全

线程安全

线程安全含义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

Java 语言中的线程安全

按照线程安全的“安全程度”由强至弱排序,我们可以将 Java 语言中各种操作共享的数据分为以下 5 类:不可变、绝对线程安全、相对线程安全、线程兼容与线程对立。

不可变

由上可知,不可变是安全性最强的线程安全,事实上,不可变的对象一定是线程安全的,不管是方法实现还是方法调用,都不需要再采取任何方式来维护线程安全。

对于基本数据类型和引用数据类型的处理方式有所不同。

如果共享数据是一个基本数据类型,那么只要在定义时使用 final 关键字修饰它就可以保证它是不可变的。

如果共享数据是一个引用数据类型(实例对象),那就需要保证对象的行为不会对其状态产生任何影响才行(如 java.lang.String 类的对象,它是一个不可变对象,们调用它的 substring()、replace() 和 concat() 这些方法都不会影响它原来的值,只会返回一个新的构造的字符串对象)。

附加补充:String 类对象是不可变的,StringBuilder 和 StingBuffer 是可变的,其中,StringBuilder 是线程不安全的,StringBuffer 是线程安全的。

保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为 final,这样在构造函数结束之后,它就是不可变的,如下代码所示, java.lang.Integer 构造函数所示的,它通过将内部状态变量 value 定义为 final 来保障状态不变。

在 Java API 中符合不可变要求的类型,除了上面提到的 String 之外,常用的还有枚举类型,以及 java.lang.Number 的部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。

绝对线程安全

绝对的线程安全完全满足 Brian Goetz 给出的线程安全的定义(即当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的),这个定义其实是很严格的。

一个类要达到 “不管运行时环境如何,调用者都不需要任何额外的同步措施” 通常需要付出很大的,甚至有时候是不切实际的代价。在 Java API 中标注自己是线程安全的类,大多数都不是绝对的线程安全。我们可以通过 Java API 中一个不是 “绝对线程安全” 的线程安全类来看看这里的 “绝对” 是什么意思。

如果说 java.util.Vector 是一个线程安全的容器,相信所有的 Java 程序员对此都不会有异议,因为它的 add()、get() 和 size() 这类方法都是被 synchronized 修饰的,尽管这样效率很低,但确实是安全的。但是,即使它所有的方法都被修饰成同步,也不意味着调用它的时候永远都不需要同步手段了,且看代码。

测试代码:(对于 vector 框架,removeThread 是写操作,printThread 是读操作)


输出结果:

疑问:这个错误是数组越界,就是说调用 get(i)方法的时候,访问的是一个不存在的元素,为什么会出问题呢?

问题描述:就是说某个序号为 i 的元素在 removeThread 线程中被删除了,但是后来 printThread 再去访问这个序号为 i 的元素,所以数组越界,这里用图来解释。

解决方式:对于两个 run 方法的方法体用 synchronized 包裹一层代码块,如下:

这样一来,一定要 removeThread 执行完后,然后 removeThread 和 printThread 才能再次竞争锁,保证操作安全。

相对线程安全

相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。上面的对于 removeThread printThread 操作 Vector 容器就是一种相对线程安全,虽然 vector.remove(i) vector.get(i)方法本身是线程安全,但是 for 循环线程不安全,所以造成错误。

上面的例子告诉我们,相对线程安全确实比绝对线程安全要低一个安全级别,绝对线程安全程序员可以啥事不管,放心地用,但是相对线程安全(以线程安全的容器为例),使用的同时程序员需要关注具体程序。

在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。

相对线程安全,以 Vector 集合框架为例,只是保证这个框架对象的单独操作(指某个函数 get remove add 在函数内的处理逻辑是安全的),像上面的程序一样,remove(i) 和 get(i) 的方法内部逻辑确实是线程安全的(即线程同步的),但是外层 for 循环、变量 i 并不是线程同步的,正是因为 i 变量没有线程间同步,所以 get(i)出现数组越界。解决方案中给 for 循环加上 synchronized 包裹一层,使其线程同步,就解决了。

线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 大部分的类都是属于线程兼容的,如集合框架类 ArrayList 和 HashMap 等。

线程对立

线程对立是安全级别最弱的一种共享数据操作,它是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。

简单的理解:线程对立是指走向了与多线程安全对立的一面,永远达不到线程安全,这是程序员所不愿意看到的,多线程开发中一定不能使用线程对立的类与方法。

一个线程对立的例子是 Thread 类的 suspend() 和 resume() 方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的,如果 suspend() 中断的线程就是即将要执行 resume() 的那个线程,那就肯定要产生死锁了。

也正是由于这个原因,suspend() 和 resume() 方法已经被 JDK 声明废弃(@Deprecated)了。常见的线程对立的操作还有 System.setIn()、System.setOut() 和 System.runFinalizerosOnExit() 等。

线程安全的实现方式(阻塞同步+非阻塞同步+无同步方案)

我们现在的问题是如何实现线程安全/线程安全的实现方式,我们从两个角度来看这个问题。

从程序员代码角度来看:程序员努力确保自己的代码没有线程同步和通信问题,不会出现代码层面的线程安全问题,更不会出现死锁问题;

从 JVM 底层保障机制来看:JVM 提供同步机制;’

本文的重点是 JVM,所以我们从 JVM 底层来看线程安全的实现方式——同步机制。其中,同步机制包括阻塞同步、非阻塞同步、无同步机制。

阻塞同步(又称互斥同步)

同步含义:是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。

互斥含义:是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。

因此,同步和互斥的关系是:互斥是因,同步是果;互斥是方法,同步是目的。

在 Java 中,最基本的互斥同步手段就是 synchronized 关键字,synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码都需要一个 reference 类型的参数来指明要锁定和解锁的对象。

如果 Java 程序中的 synchronized 明确指定了对象的参数,那就是这个对象的 reference;如果没有明确指定,那就根据 synchronized 修饰的是实例方法还是类方法,去取对应的对象实例或 Class 对象来作为锁对象。

根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加 1,相应的,在执行 monitorexit 指令时将锁计数器减 1,当计数器为 0 时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

在虚拟机规范对 monitorenter 和 monitorexit 的行为描述中,有两点是需要特别注意的。首先,synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

加粗的解释是:

1、synchronized 关键字实现的同步锁是一种互斥同步,这种互斥同步是线程间的互斥,一个线程互斥其他线程,当一个线程拿到互斥锁后,其他线程被阻挡在外面,这就是线程间互斥,但是在同一个线程内,是不存在这种同步锁互斥的。

2、顺序是 获得锁(其他线程获得锁失败,阻塞)----运行同步方法-----运行完成同步方法-----释放锁(所有线程可以重新开始竞争同步锁)

在前面讲过,Java 的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块(如何被 synchronized 修饰的 getter() 或 setter() 方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。

所以 synchronized 是 Java 语言中一个重量级(Heavyweight)的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。

除了 synchronized 之外,我们还可以使用 java.util.concurrent(下文成 J.U.C)包中的重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock 与 synchronized 很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为 API 层面的互斥锁(lock() 和 unlock() 方法配合 try/finally 语句块来完成),另一个表现为原生语法层面的互斥锁。

非阻塞同步

从阻塞同步到非阻塞同步、从悲观锁到乐观锁

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。从处理问题的方式上来说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止,如 CAS),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。

从硬件上来确定操作的原子性(如果某条指令在硬件层面上是原子操作,就一定是原子操作了):

测试并设置(Test-and-Set)、

获取并增加(Fetch-and-Increment)、

交换(Swap)、

比较并交换(Compare-and-Swap,简称 CAS)、

加载链接/条件存储(Load-Linked/Store-Conditional,简称 LL/SC)。

对于上面五条指令,都是原子操作,这里重点介绍第四条——比较并交换(Compare-and-Swap,简称 CAS)。

CAS 引入,什么是 CAS?

CAS 是 Compare And Swap 的缩写,是以一种无锁的方式实现并发控制,是区别于 synchronouse 同步锁的一种乐观锁。synchronized 是一种悲观锁,它会导致其他所有需要锁的线程挂起。

CAS 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和新值(用 B 表示)。CAS 指令执行时,当且仅当 V 符合旧预期值 A 时,处理器用新值 B 更新 V 的值,否则它就不执行更新,但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程(指粗体标记过程、CAS 指令执行过程)是一个原子操作。且用程序看一个 CAS 操作:

为什么使用原子类 AtomInteger 可以完成?

一句话解释:synchronized == for + if(cas 线程安全判断),在 incrementAndGet()方法中使用 for 循环 + if(cas 判断)包裹,保证线程安全,所以最后等于 20000,所以 incrementAndGet()方法中的 for + if(cas 线程安全判断) 保证了线程安全。

在一个无限循环中,不断尝试将一个比当前值大 1 的新值赋给自己。如果失败了,那说明在执行“获取-设置”操作的时候值已经有了修改,于是再次循环进行下一次操作,直到设置成功为止。

CAS 操作的“ABA”问题:

CAS 并不是绝对安全的原子操作,存在这样的一个逻辑漏洞:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然为 A 值,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。这个漏洞称为 CAS 操作的“ABA”问题。

当然,我们上面这个 20 个线程每个线程 i++一万遍的程序中,由于 race 的变化全部都是一个方向递增的,所以这里不存在 ABA 问题,我们不再谈论,关于 ABA 问题,笔者以后有单独的博客讲述。

无同步方案(可重入代码+线程本地存储)

要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的,笔者简单地介绍其中的两类。

第一类,可重入代码(Reentrant Code)

含义:又称纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。

可重入代码与线程安全的关系:相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。

如何判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。

第二类,线程本地存储(Thread Local Storage)

含义:就是把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

线程本地存储与线程安全的关系:由含义可知,只有共享数据可见范围限制在同一个线程中,则可完全保证线程安全。

符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如 “生产者 - 消费者” 模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的 “一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端都可以使用线程本地存储来解决线程安全问题。

Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为 “易变的”;如果一个变量要被某个线程独享,Java 中就没有类似 C++ 中的 __declspec(thread) (注:在 Visual C++ 是 “__declspec(thread)” 关键字,而在 GCC 中是 “__thread”)这样的关键字,不过还是可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。

每一个线程的 Thread 对象中都有一个 ThreadLocalMap 对象,这个对象存储了一组以上 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值得 K-V 值对,ThreadLocal 对象就是当前线程的 ThreadLocalMap 的访问入口,每一个 ThreadLocal 对象都包含了一个独一无二的 threadLocalHashCode 值,使用这个值就可以在线程 K-V 值对中找回对应的本地线程变量。

今日份分享已结束,请大家多多包涵和指点!

用户头像

还未添加个人签名 2021.04.20 加入

Java工具与相关资料获取等WX: gsh950924(备注来源)

评论

发布
暂无评论
我熬夜撸完了这篇万字Java线程笔记,助我轻松拿到30k的字节offer