springboot第76集:线程,ThreadGroup
导出数据: 查询结果可以使用脚本或工具(如 Python 的 Pandas 库)将数据导出为 Excel 格式。例如,使用 Python:
当查询数组中有大量数据(如一千多条)时,可以使用 _mget
(多获取)API 来优化性能。以下是如何优化你的查询:
使用 _mget
查询
将 order_ids
数组传入 _mget
,这样可以批量获取数据,而不是逐条查询。
批处理操作系统,把⼀系列需要操作的指令写下来,形成⼀个清单,⼀次性
交给计算机。⽤户将多个需要执⾏的程序写在磁带上,然后交由计算机去读取并逐
个执⾏这些程序,并将输出结果写在另⼀个磁带上。
程序有时会由于I/O操作、⽹络等
原因阻塞,所以批处理操作效率也不⾼。
进程单独占有⼀定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和
栈信息,还需要资源的分配回收以及⻚调度,开销较⼤;线程只需要保存寄存
器和栈信息,开销较⼩。
寄存器是cpu内部的少量的速度很快的闪存,通常存储和访问计算过程的中
间值提⾼计算机程序的运⾏速度。
程序计数器是⼀个专⽤的寄存器,⽤于表明指令序列中 CPU 正在执⾏的位
置,存的值为正在执⾏的指令的位置或者下⼀个将要被执⾏的指令的位置,
具体实现依赖于特定的系统。
yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿
意让出对当前处理器的占⽤。这⾥需要注意的是,就算当前线程调⽤了yield()
⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的
join():使当前线程等待另⼀个线程执⾏完毕之后再继续执⾏,内部调⽤的是
Object类的wait⽅法实现的
Runnable接⼝出现,降低了线程对象和线程任务的耦合性。
如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更
为轻量。
开启⼀个线程去执⾏⼀个任
务,并且这个任务执⾏完成后有⼀个返回值。
Callable、Future与FutureTask
JDK提供了 Callable 接⼝与 Future 类为我们解决这个问题,这也是所谓的“异步”
模型。
cancel ⽅法是试图取消⼀个线程的执⾏。
注意是试图取消,并不⼀定能取消成功。因为任务可能已完成、已取消、或者⼀些
其它因素不能取消,存在取消失败的可能。 boolean 类型的返回值是“是否取消成
功”的意思。参数 paramBoolean 表示是否采⽤中断的⽅式取消线程执⾏。
所以有时候,为了让任务有能够取消的功能,就使⽤ Callable 来代替 Runnable 。
如果为了可取消性⽽使⽤ Future 但⼜不提供可⽤的结果,则可以声明 Future<?
形式类型、并返回 null 作为底层任务的结果。
state表示任务的运⾏状态,初始状态为NEW。运⾏状态只会在set、
setException、cancel⽅法中终⽌。COMPLETING、INTERRUPTING是任
务完成后的瞬时状态。
Java中⽤ThreadGroup来表示线程组,我们可以使⽤线程组对线程进⾏批量控制。
ThreadGroup和Thread的关系就如同他们的字⾯意思⼀样简单粗暴,每个Thread必
然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。执⾏main()
⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程
(当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组。
ThreadGroup管理着它下⾯的Thread,ThreadGroup是⼀个标准的向下引⽤的树状
结构,这样设计的原因是防⽌"上级"线程被"下级"线程引⽤⽽⽆法有效地被GC回
收。
Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10
级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给
操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系
统决定。
线程的执⾏顺序由调度程序来决定,线程的优先级会
在线程被调⽤之前设定。
Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给
操作系统⼀个建议,操作系统不⼀定会采纳。⽽真正的调⽤顺序,是由操作系统的
线程调度算法决定的。
sleep⽅法是不会释放当前的锁的,⽽wait⽅法会。
wait可以指定时间,也可以不指定;⽽sleep必须指定时间。
wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易
死锁。
wait必须放在同步块或同步⽅法中,⽽sleep可以再任意位置
ThreadLocal是⼀个本地线程副本变量⼯具类。内部是⼀个弱引⽤的Map来维护。
有些朋友称ThreadLocal为线程本地变量或线程本地存储。严格来说,ThreadLocal
类并不属于多线程间的通信,⽽是让每个线程有⾃⼰”独⽴“的变量,线程之间互不
影响。它为每个线程都创建⼀个副本,每个线程可以访问⾃⼰内部的副本变量。
这个例子展示了如何使用 ThreadLocal
来为每个线程提供独立的变量副本。在多线程环境中,ThreadLocal
的作用是为每个线程维护自己的值副本,而不会相互干扰。以下是对代码的详细中文注解:
1. ThreadLocal<String> threadLocal = new ThreadLocal<>();
这行代码创建了一个 ThreadLocal
对象,它会为每个线程单独维护一个 String
类型的变量。ThreadLocal
的作用是在线程之间隔离数据,避免不同线程访问相同的共享变量时相互干扰。
2. ThreadA
和 ThreadB
类
这两个类分别实现了
Runnable
接口,代表两个不同的线程任务。它们都通过构造函数接收一个
ThreadLocal<String>
对象,并在各自的run()
方法中进行操作。
3. threadLocal.set("A")
和 threadLocal.set("B")
ThreadA
中调用了threadLocal.set("A")
,设置当前线程的ThreadLocal
变量为"A"
。同理,
ThreadB
中调用了threadLocal.set("B")
,设置当前线程的ThreadLocal
变量为"B"
。由于
ThreadLocal
为每个线程维护独立的值,所以ThreadA
和ThreadB
中对ThreadLocal
的设置互不干扰。
4. Thread.sleep(1000);
这里让线程睡眠 1 秒,用于模拟某种耗时的操作。
即使线程处于睡眠状态,每个线程的
ThreadLocal
值依然是独立的,不会被另一个线程的修改影响。
5. System.out.println("ThreadA输出:" + threadLocal.get());
在线程睡眠结束后,通过
threadLocal.get()
方法获取当前线程的ThreadLocal
值。由于
ThreadA
和ThreadB
各自在线程中调用了threadLocal.set()
设置不同的值,所以它们输出的结果不同。
6. new Thread(new ThreadA(threadLocal)).start();
这行代码启动了一个新的线程,该线程运行
ThreadA
的任务,并操作传入的threadLocal
变量。
7. 程序输出
程序启动了两个线程,分别运行 ThreadA
和 ThreadB
,由于 ThreadLocal
能保证每个线程的独立性,输出结果会是:
css
代码解读
复制代码
ThreadA输出:A
ThreadB输出:B
最常⻅的ThreadLocal使⽤场景为⽤来解决数据库连接、Session管理等。数据库连
接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明⼀些
私有变量来进⾏操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭
连接。
InheritableThreadLocal类与ThreadLocal类稍有不同,Inheritable是继承的意思。
它不仅仅是当前线程可以存取副本值,⽽且它的⼦线程也可以存取这个副本值。
并发编程模型的两个关键问题
线程间如何通信?即:线程之间以何种机制来交换信息
线程间如何同步?即:线程以何种机制来控制不同线程间操作发⽣的相对顺序
有两种并发模型可以解决这两个问题:
消息传递并发模型
共享内存并发模型
这两种模型之间的区别如下表所示:
内存可⻅性是针对的共享变量。
现代计算机为了⾼效,往往会在⾼速缓存区中缓存共享变量,因为cpu访
问缓存区⽐访问内存要快得多。
线程之间的共享变量存在主内存中,每个线程都有⼀个私有的本地内存,存
储了该线程以读、写共享变量的副本。本地内存是Java内存模型的⼀个抽象
概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等。
Java线程之间的通信由Java内存模型(简称JMM)控制
从图中可以看出:
所有的共享变量都存在主内存中。
每个线程都保存了⼀份该线程使⽤到的共享变量的副本。
如果线程A与线程B之间要通信的话,必须经历下⾯2个步骤:
i. 线程A将本地内存A中更新过的共享变量刷新到主内存中去。
ii. 线程B到主内存中去读取线程A之前已经更新过的共享变量。
所以,线程A⽆法直接访问线程B的⼯作内存,线程间通信必须经过主内存。
注意,根据JMM的规定,线程对共享变量的所有操作都必须在⾃⼰的本地内存中进
⾏,不能直接从主内存中读取。
所以线程B并不是直接去主内存中读取共享变量的值,⽽是先在本地内存B中找到
这个共享变量,发现这个共享变量已经被更新了,然后本地内存B去主内存中读取
这个共享变量的新值,并拷⻉到本地内存B中,最后线程B再读取本地内存B中的新
值。
JMM通过控制主内存与每个线程的本地内存之间的交
互,来提供内存可⻅性保证。
Java中的volatile关键字可以保证多线程操作共享变量的可⻅性以及禁⽌指令
重排序,synchronized关键字不仅保证可⻅性,同时也保证了原⼦性(互斥
性)。在更底层,JMM通过内存屏障来实现内存的可⻅性以及禁⽌重排序。
为了程序员的⽅便理解,提出了happens-before,它更加的简单易懂,从⽽
避免了程序员为了理解内存可⻅性⽽去学习复杂的重排序规则以及这些规则
的具体实现⽅法。
JMM与Java内存区域划分的区别与联系
两者是不同的概念层次。JMM是抽象的,他是⽤来描述⼀组规则,通过这个规
则来控制各个变量的访问⽅式,围绕原⼦性、有序性、可⻅性等展开的。⽽
Java运⾏时内存的划分是具体的,是JVM运⾏Java程序时,必要的内存划分。
都存在私有数据区域和共享数据区域。⼀般来说,JMM中的主内存属于共享数
据区域,他是包含了堆和⽅法区;同样,JMM中的本地内存属于私有数据区
域,包含了程序计数器、本地⽅法栈、虚拟机栈
为什么指令重排序可以提⾼性能?
简单地说,每⼀个指令都会包含多个步骤,每个步骤可能使⽤不同的硬件。因此,
流⽔线技术产⽣了,它的原理是指令1还没有执⾏完,就可以开始执⾏指令2,⽽不
⽤等到指令1执⾏结束之后再执⾏指令2,这样就⼤⼤提⾼了效率。
但是,流⽔线技术最害怕中断,恢复中断的代价是⽐较⼤的,所以我们要想尽办法
不让流⽔线中断。指令重排就是减少中断的⼀种技术。
先加载b、c(注意,即有可能先加载b,也有可能先加载c),但是在执⾏add(b,c)
的时候,需要等待b、c装载结束才能继续执⾏,也就是增加了停顿,那么后⾯的指
令也会依次有停顿,这降低了计算机的执⾏效率。
为了减少这个停顿,我们可以先加载e和f,然后再去加载add(b,c),这样做对程序(串
⾏)是没有影响的,但却减少了停顿。既然add(b,c)需要停顿,那还不如去做⼀些有
意义的事情。
综上所述,指令重排对于提⾼CPU处理性能⼗分必要。虽然由此带来了乱序的问
题,但是这点牺牲是值得的。
指令重排⼀般分为以下三种:
编译器优化重排
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执⾏顺序。
指令并⾏重排
现代处理器采⽤了指令级并⾏技术来将多条指令重叠执⾏。如果不存在数据依
赖性(即后⼀个执⾏的语句⽆需依赖前⾯执⾏的语句的结果),处理器可以改变
语句对应的机器指令的执⾏顺序。
内存系统重排
由于处理器使⽤缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看
上去可能是在乱序执⾏,因为三级缓存的存在,导致内存与缓存的数据同步存
在时间差。
指令重排可以保证串⾏语义⼀致,但是没有义务保证多线程间的语义也⼀致。所以
在多线程下,指令重排序可能会导致⼀些问题。
顺序⼀致性模型与JMM的保证
顺序⼀致性模型是⼀个理论参考模型,内存模型在设计的时候都会以顺序⼀致性内
存模型作为参考。
Java内存模型(JMM)对于正确同步多线程程序的内存⼀致性做了以下保证:
如果程序是正确同步的,程序的执⾏将具有顺序⼀致性。 即程序的执⾏结果
和该程序在顺序⼀致性模型中执⾏的结果相同。
这⾥的同步包括了使⽤ volatile 、 final 、 synchronized 等关键字来实现多线程
下的同步。
如果程序员没有正确使⽤ volatile 、 final 、 synchronized ,那么即便是使⽤了
同步(单线程下的同步),JMM也不会有内存可⻅性的保证,可能会导致你的程序
出错,并且具有不可重现性,很难排查。
在当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这
个写操作仅对当前线程可⻅;从其他线程的⻆度来观察,这个写操作根本没有被当
前线程所执⾏。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写
操作才对其他线程可⻅。在这种情况下,当前线程和其他线程看到的执⾏顺序是不
⼀样的。
未同步程序在JMM和顺序⼀致性内存模型中的执⾏特性有如下差异:
顺序⼀致性保证单线程内的操作会按程序的顺序执⾏;JMM不保证单线程内的
操作会按程序的顺序执⾏。(因为重排序,但是JMM保证单线程下的重排序不
影响执⾏结果)
顺序⼀致性模型保证所有线程只能看到⼀致的操作执⾏顺序,⽽JMM不保证所
有线程能看到⼀致的操作执⾏顺序。(因为JMM不保证所有操作⽴即可⻅)
JMM不保证对64位的long型和double型变量的写操作具有原⼦性,⽽顺序⼀致
性模型保证对所有的内存读写操作都具有原⼦性。
happens-before关系的定义如下:
如果⼀个操作happens-before另⼀个操作,那么第⼀个操作的执⾏结果将对第
⼆个操作可⻅,⽽且第⼀个操作的执⾏顺序排在第⼆个操作之前。
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必
须要按照happens-before关系指定的顺序来执⾏。如果重排序之后的执⾏结
果,与按happens-before关系来执⾏的结果⼀致,那么JMM也允许这样的重
排序。
在Java中,有以下天然的happens-before关系:
程序顺序规则:⼀个线程中的每⼀个操作,happens-before于该线程中的任意
后续操作。
监视器锁规则:对⼀个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对⼀个volatile域的写,happens-before于任意后续对这个
volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens
before C。
start规则:如果线程A执⾏操作ThreadB.start()启动线程B,那么A线程的
ThreadB.start()操作happens-before于线程B中的任意操作、
join规则:如果线程A执⾏操作ThreadB.join()并成功返回,那么线程B中的
任意操作happens-before于线程A从ThreadB.join()操作成功返回。
注意,真正在执⾏指令的时候,其实JVM有可能对操作A & B进⾏重排序,因为⽆
论先执⾏A还是B,他们都对对⽅是可⻅的,并且不影响执⾏结果。
如果这⾥发⽣了重排序,这在视觉上违背了happens-before原则,但是JMM是允许
这样的重排序的。
所以,我们只关⼼happens-before规则,不⽤关⼼JVM到底是怎样执⾏的。只要确
定操作A happens-before操作B就⾏了。
重排序有两类,JMM对这两类重排序有不同的策略:
会改变程序执⾏结果的重排序,⽐如 A -> C,JMM要求编译器和处理器都禁⽌
这种重排序。
不会改变程序执⾏结果的重排序,⽐如 A -> B,JMM对编译器和处理器不做要
求,允许这种重排序。