并发编程基础
上下文切换
上下文切换是CPU中一个消耗较大的操作,要尽量减少上下文切换,一些方法:
- 无锁并发编程。避免使用锁,用Hash等方法,分散竞争
- CAS算法。也叫乐观锁算法,低冲突的时候特别高效率
- 避免创建不必要的线程。
- 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换(稍后了解)
生产环境中减少上下文切换可以用jstack dump线程快照,并分析原因
死锁
同样可以通过jstack分析
JAVA并发机制的底层实现原理
volatile的应用
某线程对volatile 变量进行写入操作后,其他线程读取volatile变量时,能获得volatile的最新值。
X86汇编实现原理:写入volatile变量时,JVM会向处理器发送一条LOCK前缀的指令,将这个变量对应的缓存行写入主存。同时,其他的处理器会嗅探总线上传播的数据以此判断自己的缓存是否过期,若发现过期,则将对应的缓存行置为失效,更多内容参考 缓存一致性(Cache Coherency)入门
synchronized的实现原理与应用
基础设定:每一个JAVA对象都可以作为锁
- 对于实例同步方法,锁是当前实例对象
- 对于普通静态同步方法,锁是当前的类Class对象
- 对于同步方法块,锁是括号里指定的对象
JAVA对象头
synchronized用的锁是存在JAVA对象头里的。
若对象为数组,那么 对象头长度为 3字宽(1字宽 为 32bit/64bit 视虚拟机位数而定) 若对象非数组,那么 对象头长度为 2字宽
JAVA对象头
位置 | 内容 | 描述 |
---|---|---|
第一字宽 | MarkWord | 存储对象的hashCode或锁信息 |
第二字宽 | Class Metadata Address | 存储到对象类型数据的指针 |
第三字宽 | Array Length | 数组的长度(如果对象是数组) |
MarkWord里,最后两位为 锁标志位,Markword其余位置 存储信息的含义 会随着 锁标志位 的改变而改变。64位与32位JVM存储的Markword格式不一样
32位JVM锁标志位 的含义:
- 01 及偏向锁位置为0 则为 无锁状态
- 01 及偏向锁位置为1 则为 偏向锁状态
- 00 表示轻量级锁
- 10 表示重量级锁
- 11 表示GC标志
锁的升级与对比
JAVA SE 1.6为减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁” 。 在JAVA SE 1.6 中,锁一共有4种状态:无锁,偏向锁,轻量级锁,重量级锁。这几个状态会随着竞争情况逐渐升级。锁从偏向锁升级到非偏向锁后,不会再变成偏向锁。
1.重量级锁
使用系统的互斥量实现。 使用睡眠唤醒机制竞争锁,适用于高竞争的场景,追求高吞吐量。
2.轻量级锁
使用自旋提高竞争效率,追求高响应。当另外一个进程竞争一个已被当前线程获取的锁时,锁会膨胀,升级成重量级锁。升级成重量级锁后,释放锁后,再获取锁,会再次变成轻量级锁。
轻量级锁及重量级锁 都会将markword写入到对应获取了锁的线程的栈桢里,并在markword里保存一个指针,指向 对应的栈桢。释放锁时,把这个栈桢保存的内容覆盖回 markword里
3.偏向锁
经HotSpot作者研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁
线程使用CAS向一个可偏向,且未偏向的对象 竞争偏向锁,若竞争成功,则在markword上写上自己的线程编号,后续访问检查markword,如果依然markword还是写着自己的线程编号,则无需CAS竞争偏向锁。
若有另外一个线程竞争这个锁时,偏向锁就会视情况,看当前是否处于锁定状态,若是,则偏向锁变成轻量级锁,若否,则变成无锁状态,但markword会标志其不可偏向。
锁的优缺点对比
锁类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
重量级锁 | 在竞争大的时候,使用唤醒机制能节约CPU资源 | 在竞争较少的情况下,重量级锁会使得线程进入等待状态。当有线程释放锁资源时,才会唤醒线程,让线程竞争锁 | 悲观锁思想,适用于竞争大,追求吞吐量的场景 |
轻量级锁 | 在竞争小的时候,线程可以快速获得锁,免去进入等待队列的时间 | 当竞争大的时候会加重CPU消耗 | 采用乐观锁思想,适用于竞争较少的场景 |
偏向锁 | 在无竞争的情况下,偏向锁的性能可接近非同步方法的性能 | 当竞争发生时,偏向锁的撤销需等到全局安全点,竞争消耗较大 | 适用于无线程竞争的场景 |
原子操作的实现原理
CPU实现原子操作的方式
- 总线锁(锁住时,其他核都不能访问任意内存)
- 缓存锁(只锁特定的缓存行。但存在不能锁缓存行的情况,如要锁的数据大于一个缓存行)
JAVA实现原子操作
- 使用循环CAS(利用CPU提供的CMPXCHG指令)
- 存在问题:
- ABA问题
- 自旋CAS损耗CPU
- 只能保证一个共享变量的原子操作
使用锁机制实现原子操作
JAVA内存模型
JAVA内存模型的基础
并发编程模型的两个关键问题
- 线程之间如何通讯
- 一种形式:共享内存(通过写读内存中的公共状态进行隐式通讯)
- 另一种形式:消息传递(没有公共状态,通过发送消息来显示进行通讯)
- 线程之间如何同步(用于控制线程间操作发生相对顺序的机制)
- 共享内存的并发模型里,同步是显式进行的(程序员必须显式指定某些操作的互斥执行)
- 消息传递的并发模型里,同步是隐式进行的(消息发送一定在消息接收前)
JAVA内存的抽象结构
每个线程都有其独立的 工作内存,然后有一个公共的主存。
线程写入一个共享变量何时对另外一个线程可见由JMM决定。
指令重排序
指令可能重排序的位置:
编译重排序:在不改变单线程程序语义的情况下,改变指令执行顺序 CPU指令重排序:在不影响数据依赖的情况下,对CPU指令重排序,以提高CPU流水线使用效率。意思跟编译重排序差不多 内存系统重排序:CPU通过缓存操作主存,且读写缓存都是批量的。因此这个也算是指令重排序。
JMM会根据自身的定义,在某些代码位置插入一些 内存屏障(memory barriers) 阻止一些特定的重排序,这里的 重排序指代的是 主存 的重排序,而非CPU缓存中操作的重排序。
并发编程模型的分类
现代的CPU都会使用缓存来填补 CPU操作与内存操作之间的速度差异。CPU可以批量读取数据到缓存中,也可以批量将数据写回到内存里。
那么这样一种场景就出现了:CPU写了缓存,但还没会写到内存,此时又需要从主存中读取数据加载到缓存中。
如果上述场景要求:
-
加载内存数据到缓存前,必须回写缓存到内存中,那么CPU整体的性能将会大大降低,无法充分发挥缓存的作用。
-
无需写回内存就可以从内存加载新数据的话能最大程度利用缓存的性能,但是!这样的话对于 内存的实际操作顺序就由 程序代码定义中的 先写后读 变成了 先读后写!当有其他线程依赖于这些 读/写 的变量进行逻辑判断的话,就会出现难以理解的现象。
然而由于第二种选择能大大提升CPU的效率,因此,基本所有CPU都允许2中的情况出现,也就是说,允许 写-读 重排序出现。当然,这种重排序允许的前提是不修改单线程的语义,也就是在单线程中,写-读重排序操作之间没有数据依赖关系。
可以猜测出除了,写读重排序外,还有另外三种重排序形式,一共四种重排序
- 读读
- 读写
- 写写
- 写读
按照我们之前的分析,我们可以判断,读读,写写这两种重排序对 优化缓存使用方面都没有较大的作用,但或许能为CPU流水线乱序执行提供较为宽松的支持。
至于读写重排序,可以优化的场景就是,缓存满了,还要读入新的数据,这时就把一些写操作提前进行,更改完后写回内存,再读取内存数据覆盖写回了的那部分的缓存
根据那种重排序规则是允许的,设定了几种并发编程模型。
只允许 写读 重排序的:X86,SPARC-TSO 允许所有的重排序的:IA64,PowerPC
上面的重排序都是性能优化项,但如果涉及到线程同步,那么必须在某些关键的时序中禁止重排序,这样其他线程才能准确的获得数据状态。为了禁止重排序,内存屏障就被设计出来了。
JMM定义了以下四种内存屏障:
- 读读屏障
- 写写屏障
- 读写屏障
- 写读屏障
其中写读屏障同时具有其他3种屏障的作用。同时该屏障也是一个消耗很大的操作,因为需要将所有的缓存回写到内存中。
Happens-before
happens-before是一系列规则,它定义了哪些操作有happens-before的关系。若A happens-before B,那么A所有操作都是对B可见的。
一个Happens-before规则的背后对应的就是 JMM调整编译器重排序,调整CPU重排序的过程。
JMM通过happens-before规则将程序员从繁琐的重排序规则中解脱出来。
顺序一致性
顺序一致性内存模型是一个理论参考模型,这个模型最为符合常人的逻辑思维习惯。在这个模型中,所有线程对内存的操作都马上对其他线程可见,且线程内的操作不会进行重排序,所有线程的所有操作都是串行执行的,不存在并发的关系。
数据竞争与顺序一致性
JMM定义数据竞争: 线程A写变量X 线程B读变量X 且读写操作之间没有通过同步来排序
当没有正确同步时,程序会出现出一些难以理解的结果。但只要进行正确同步,那么JMM将会保证其执行结果与在 顺序一致性内存模型下 执行的结果一致
Volatile的内存语义
一个声明为volatile的变量 对其进行读取操作时,总能获得其他线程修改的最新的值 对volatile变量的读写是原子的,包括double和long
从JDK5开始,volatile变量的写读可以实现线程之间的通讯,即通过volatile的读写能建立起不同线程事件之间的happens-before关系
- volatile变量的写 与 锁的释放 有相同的内存语义,JMM会把当前线程的本地内存都刷新到主存中去
- volatile变量的读 与 锁的获取 有相同的内存语义。JMM会把该线程对应的所有本地内存都置为无效,然后从主存中读取所有所需的数据
写读volatile变量实际上是一个消息发送的过程。
volatile内存语义的实现
volatile变量写-读之间要组成一个happens-before的关系的话
volatile写之前要加入一个store-store屏障,volatile写之后要加一个store-load屏障
- volatile写之前 加入store-store屏障是为了防止volatile写的变量 先于 代码顺序中的其他写操作写到内存中,实现happens-before关系。
- volatile写之后 加入store-load屏障是为了防止 volatile写之后的 读/写 语句先于volatile的结果写入到内存,store-load是一个全能屏障,防止后续写操作重排序的原因也是为了实现happens-before,至于防止后续的读操作的重排序,也是为了实现happens-before,防止一种情况出现:提前读取本应被其他线程修改的变量(其他线程修改的依据是本线程写入的volatile写而构建的happens-before关系)
volatile读之后要加入一个loadload屏障以及loadStore屏障
- loadload,loadStore屏障防止 volatile读后面的 读操作先于volatile读,如果顺序反了,那么happens-before关系就不成立了
- 为什么不防止代码顺序中 volatile前的读在执行时后于volatile读?是因为这里只需要保证 代码顺序中 volatile读后的操作 后于 volatile写?
在X86的实现里,只会出现 写-读重排序(写读重排序最能提高缓存使用效率,效能提高最大),因此X86JVM只需要处理掉store-load重排序即可。因此在X86处理器中,写volatile的消耗较大,而读volatile基本与正常变量读取的消耗一致。
为什么要增强VOLATILE语义
在JSR-133之前,volatile变量的读写只管其自身的读写,而不保证其他变量也刷新到内存(即不保证happens-before关系),但为了实现HAPPENS-BEFORE关系,提供一个更简易统一的编程模型,因此专家们做了增强volatile语义的调整
锁的内存语义
锁的获取-释放会建立起happens-before关系
-
当线程获取锁时,JMM会把线程对应的本地内存置为无效,从而使得被monitor保护的临界区代码必须从主内存中读取共享变量。与volatile读一致
-
当线程释放锁时,JMM会把本地内存中的所有共享变量刷新到内存中。与volatile写一致
以上这么一个 写内存 读内存的过程 本质上是消息传递的过程。
锁内存语义的实现
以ReentranceLock的实现来讲述
ReentranceLock继承自Lock接口,ReetranceLock内部有一个抽象类Sync继承自AbstractQueuedSynchroinzer(简称AQS,其核心是维护了一个volatile变量state)。Sync抽象类下有两个实现,一个是公平锁实现,一个是非公平锁的实现。
我们先看公平锁的实现,
tryAcquire是获取锁的基本操作,其声明自AQS,各种乐观、悲观的锁获取实现都会调用tryAcquire
伪代码:
boolean tryAcquire(int i){
if(AQS.state == 0){
if(锁获取的排队队列没有人在排队或自己是排队中的第一人 && CAS AQS的STATE为i成功){
将锁所归属的线程置为本线程
return true;
}else{
return false;
}
}else{
if(获取锁的是自己){
将AQS的state置为原值+i
return true;
}else{
return false;
}
}
释放锁tryRealease声明自ABS,当锁TOTALLY FREE的时候返回true,否则返回false.返回true是告知AQS去通知那些等待线程可以去竞争锁了。
boolean tryRealse(int i){
if(尝试释放的线程不是锁拥有者){
抛出异常
}else{
int newState = ABS.STATE-i;
boolean free = false;
if(newState == 0){
将当前锁的的拥有线程置为null
free = true;
}
将AQS的state置为newState//将这个最后赋值是为了将当前线程为null的结果一起刷新到内存中
return free;
}
}
非公平锁
非公平锁的释放实现与公平锁一样,但 获取稍有不同,非公平锁不考虑等待队列中的排队顺序
boolean tryAccquire(int i){
if(AQS.state==0){
if(获取锁的是本线程){
AQS.state = AQS.state + i;
return true;
}else{
return false;
}
}else{
if(CAS ABS.state = i 成功){
将当前线程设为AQS的互斥占有者
return true;
}else{
return false;
}
}
}
AQS.CAS的实现
AQS的CAS方法为 compareAndSetState(),其调用Unsafe类的compareAndSwapInt方法实现。这个方法有volatile变量读与写的语义。
具有volatile读与写的内存语义意味着编译器不能对 CAS与CAS前后的代码进行重排序。
这个语义的实现在X86里不由内存屏障实现,而由CPU指令 lock cmpxchg实现。lock前缀在单核CPU里是不必要的。
lock前缀的语义如下: 1.确保内存中的 读-改-写 原子执行。执行过程中会 锁总线或者锁缓存 2.静止该指令与 之前之后的指令重排序 3.把缓冲区的数据写入到内存中
concurrent包的实现
JAVA线程通讯方式有以下几种
- 线程A写volatile变量X,随后线程B读volatile变量X
- A CAS X ,B读X
- A CAS X, B CAS X
- A 写 X, B CAS X
CAS 及 volatile是整个concurrent包实现的基石 CAS 及 volatile 构成了 AQS,原子变量类,非阻塞数据解雇 然后又由这3个构成了concurrent包
final域的语义
重排序规则
对象里的基本对象:
class FinalTest{
int i;
final int j;
FinalTest(){
i = <span class="hljs-number" style="box-sizing: border-box; color: teal;">10</span>;
j = <span class="hljs-number" style="box-sizing: border-box; color: teal;">11</span>;
}
}
- 构造函数中给final对象赋值与随后把这个构造对象的引用传给一个引用变量的操作不能重排序(如果不这做,那其他线程可能访问到未初始化的final对象,实现的方法就是在final域的赋值后,初始化函数结束前 添加一个store-store屏障)
- 初次读一个包含final域的对象引用,与随后初次读这个final域,这两个操作不能重排序(在大多数处理器中,这是个有依赖关系的操作,不会被重排序,但少数处理器会对其进行重排序,因此 需要加入loadload屏障来解决这个问题,否则外部也有可能读取到未经初始化的final变量)
对象里的对象是final域的话,新增一条重排序规则:
class FinalTest2{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">final</span> <span class="hljs-built_in" style="box-sizing: border-box; color: #0086b3;">Object</span> obj;
FinalTest2(){
obj = <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">new</span> <span class="hljs-built_in" style="box-sizing: border-box; color: #0086b3;">Object</span>();
}
}
-
构造函数内一个final域对象的初始化函数执行完成 与 在构造函数外将 这个对象赋值给一个引用变量 这两个操作不能重排序,否则将有可能使得其他线程读到未初始化完成的final对象
-
要保证未经初始化的final域不被其他线程看到,实际上还需要另外一个保障:在构造函数中,不将 this的引用暴露给其他线程。如以下的初始化函数就将this暴露出去了,这可能导致外部访问到未经初始化的final域。
class FinalTest3{
final Object obj; static FinalTest3 leak;
FinalTest3(){
obj = new Object();
leak = this;
}}
有了以上的条件的保证,那么对象里的final域被其他线程可见时,都是初始化完成的
- final语义在X86中的实现:因x86只会对store-load重排序,因此无需刻意加入内存屏障即可实现final语义
Happens-before
程序员希望有一个强的内存模型,这样,编程会显得更加直观,更加便于理解。 但是编译器,优化器希望有一个弱的内存模型,这样他们的发挥空间更多,能得到更高的执行性能。
为了在性能与易用性方面取得平衡, 专家们设计了一套 Happens-before规则。程序员只要基于Happens-before规则来编写程序,那么就能够编写出符合预期的程序,Happens-before实现的具体细节由JVM屏蔽。
JMM把happens-before要求禁止的重排序分为了两类:
- 会影响执行结果的重排序
- 不会影响执行结果的重排序
对于会影响结果的重排序,JMM一定会禁止其重排序。 但对于Happens-before规则要求禁止,但不会影响(单线程)执行结果的重排序,JMM不作要求,可重排亦可不重排。
如此设计,为程序员提供了一个足够强的内存模型,也给编译器和CPU足够的自由优化空间。
Happens-before起源于Lamport的一篇论文,这篇论文可以看我博文中的对应的解析。
在JSR-133中对happens-before关系的定义如下
- 如果一个操作Happens-before另外一个操作,那么第一个操作的执行结果将对第二个操作可见,且第一个操作的执行顺序在第二个操作之前。这是对程序员的保证。
- 两个操作之间存在hb关系,并不意味着JAVA平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序后的执行结果与按hb关系来执行的结果一致,那么这个重排序不非法。这是JMM对编译器及处理器重排序的约束原则。
具体的HB规则
- 程序顺序规则,代码顺序前面的语句HB后面的语句
- 监视器锁规则,解锁HB加锁
- volatile变量规则,写HB读
- 传递性
- start()规则
- join规则
双重检查锁定
class DoubleCheckExample{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">private</span> <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">volatile</span> Object obj;
<span class="hljs-function" style="box-sizing: border-box;">Object <span class="hljs-title" style="box-sizing: border-box; color: #990000; font-weight: bold;">getLazyObj</span><span class="hljs-params" style="box-sizing: border-box;">()</span></span>{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(obj == <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">null</span>){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">synchronized</span>(<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">this</span>){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(obj == <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">null</span>){
obj = <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">new</span> Object();
}
}
}
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> obj;
}
}
上述加上volatile的作用是,防止初始化时 对obj变量赋值操作跟初始化Obj操作的 赋值重排序。
以上延时加载还可以使用基于类初始化的解决方案。
一个类,一般情况下会尽量的延迟初始,直到不初始化会影响程序的正确性为止。JVM会保证类初始化只执行一遍,因此,这个机制可以被利用作延迟初始化。实现如下:
class OutClass{
private static InnerClass{
static OutClass lazyObj = new OutClass();
}
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">public</span> <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">static</span> getOutClass{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> InnerCLass.lazyObj;
}
}
JAVA内存模型总结
顺序一致性内存模型是一个理论的参考模型,在设计JMM及CPU内存模型时都会以此为参照,并放松一些内存模型的限制,以期达到更好的性能。
对不存在依赖关系的指令:
- 允许store-load重排序,得到TSO(total store oredering)内存模型
- 1的基础上允许store-store重排序,得到PSO(partial store order)内存模型
- 继续允许 load-store 及 loadload重排序,得到RMO(Relaxed Memory Order)内存模型
由于JMM的内存模型强于大多数的CPU内存模型,因此JMM需要在特定的位置插入内存屏障,以实现JMM的Happens-before关系。JMM会通过插入内存屏障为不同平台的程序员提供一个一致的内存模型JMM.
可见性保证:
- 单线程程序不存在可见性问题
- 正确同步的多线程程序的执行将具有与顺序一致性内存模型一样的结果。
- 未同步、未正确同步的多线程程序,JMM为其提供了最小安全保障:读取到的值要么是某个线程写入的值,要么是默认值。
64位数据的非原子性,跟这个最小安全保障没关系。也就是说,这个最小安全保障并不保证不会读取到写到一半的64位数据
JSR-133对旧内存模型的修补
- 禁止了对volatile变量读写操作的一些重排序,以实现happens-before语义
- 增强final的语义,为其添加了两个重排序规则,保证了在 构造函数this不leak的情况下,final变量被其他线程访问到时,都经过了初始化。
JAVA并发编程基础
线程
线程状态:
- NEW 初始状态,线程被构建 还没有执行start方法
- RUNNABLE 运行中,包括 就绪(READY) 及 运行(RUNNING) 两种
- BLOCKED 阻塞状态,表示线程阻塞于锁
- WATING 等待状态,表示线程等待 其他的线程的通知/中断
- TIME_WATING 超时等待状体,超过一定时间后自动返回
- TERMINATED 终止状态,线程已经执行完毕。
-
NEW->RUNNABLE{Thread.start()}
-
RUNNABLE->WATING{Object.wait(),Thread.join(),LockSupport.park()}
-
WAITING -> RUNABLE {Object.notify(),Object.notifyAll(),LockSupport.unpark(Thread)}
-
RUNNABLE -> BLOCKED {等待进入synchronized方法或方法块}
-
BLOCKED -> RUNNABLE {成功获取monitor}
-
RUNABLE -> TIME_WAITING { Object.wait(long),Thread.sleep(long),LockSupport.parkNanos(long),LockSupport.parkUntil(),Thread.join(long) }
-
TIME_WAITING -> RUNNABLE {超时等待时间结束,Object.notify(),Object.notifyAll,LockSupport.unpark(Thread)}
8.RUNNABLE -> TERMINATED {程序执行完毕}
- READY -> RUNNING {系统调度}
- RUNNING -> READY {Thread.yield(),系统调度}
Daemon线程
Deamon线程是后台支持线程。当不存在前台线程,只有Daemon线程时,整个程序就会退出。
当没有前台线程时,Daemon线程有可能随时终止,不管代码执行到哪里。如在Deamon线程中有finally块,也不一定会执行。
中断
中断就是其他线程向本线程打招呼。至于打招呼后要干嘛,则自行定义。有很多声明抛出InteruptedException的方法都会将 Interupted标识复位,然后抛出异常,等待你处理。 自己在代码中也可以主动监测中断,并处理中断
中断最常用的目的是 终止/取消 任务。
过期的suspend(),resume(),stop()
被禁用的原因是suspend()执行时并没有释放锁等资源,可能造成死锁。stop()方法会马上停止线程,导致一些打开的资源没有释放。suspend,resume可以使用等待/通知机制代替
安全的终止线程
- 通过中断通知
- 通过共享变量通知
线程间通讯
volatile
synchroized
等待/通知机制
生产者消费者模式用于隔离事件 与 响应 的实现。但响应线程何时执行是一个难以处理的问题。大家希望有一个 高效的通知机制(无需自旋浪费CPU,响应时间也要足够的快),因此等待通知机制就被发明出来了。
- notify() 通知一个在对象上等待的线程,使其从wait()方法返回。而返回的前提是该线程获取到了对象的锁。
- notifyALl() 通知所有等待在该对象上的线程。
- wait()
这个机制构建于synchronized(monitor)的基础上,进行notify,wait都需要获得对应对象的monitor.线程wait之后被notify但没有获得monitor锁时,会处于blocked状态
管道输入/输出流
这个管道流 与 文件流/网络流 的最主要区别为,其用于线程间通讯,介质为内存
PipedReader/PipedWriter
Thread.join
ThreadLocal
JAVA中的锁
锁是用来控制多个线程访问共享资源的方式,其目的与synchroinzed一致。synchronized的使用更便捷;锁的使用需要显式处理比较麻烦,但其可以实现一些比较复杂的锁的形式,如
- 非阻塞地获取锁
- 能被中断地获取锁
- 超时获取锁
Lock Api
- lock()
- lockInterruptibly()
- tryLock() 尝试一次,若无法获取则马上返回
- tryLock(long time,TimeUnit unit) 超时获取锁,可被中断
- unLock()
- newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,将会释放锁。
队列同步器
AbstractQueuedSynchronizer是用来实现锁及其他同步组件的基础框架,它用state变量表示同步状态,使用内置的FIFO队列来完成资源获取线程的排队工作。
其主要使用方式是继承。同时推荐在所需实现的工具的内部,用一个内部类继承AQS,并用聚合的形式进行工作。
同步器简化了锁的实现,屏蔽了 同步状态管理,线程排队,等待与唤醒等底层操作。
同步队列
- 这个队列是一个先进先出的队列,在AQS内部用于维护同步线程的顺序,当线程尝试获取同步状态失败时,就会往这个队列通过自旋CAS在队列末加一个节点,然后自旋获取同步状态的操作,若获取失败,则阻塞,等待被唤醒(可能是同步状态释放,也可能是中断导致),唤醒后再自旋。
- 同步队列节点有以下状态:
- CANCELLED,值为1,表示由于 超时 或者 中断 等原因取消等待
- SIGNAL,值为-1,后续节点的线程处于等待的状态,当前节点 如果释放了 同步状态 或者 被取消,将会通知后续节点,使得后续节点得以运行。
- CONDITION,值为-2,节点在等待队列中
- PROPATION,值为-3,表示下一次共享式同步状态获取将会无条件的被传播下去
- INITIAL,值为0,初始状态
- 同步队列的头结点是获得锁的线程所在结点,或者刚刚释放锁的结点。但有一种情况例外,那就是队列为空,tryAcquire一次就成功,那么等待队列不会加上当前线程所在的NODE。但是缺少了这个NODE,获取同步状态失败的线程将无法统一后续处理,因此获取同步状态失败的线程发现没有头结点时,会给其初始化一个头结点,然后自己的结点加在头结点之后。然后在自旋CAS中将前置NODE的waitStatus改为signal,这样 释放同步状态时,处于等待状态的线程才会被唤醒。
- 获取锁的顺序基本上是队列中的排队顺序,除非有线程没有加入到队列中排队,直接用tryAquire(and so on...)一次就获得了锁。如ReentrantLock的公平锁和非公平锁的实现的区别就在于,公平锁在判断同步队列存在排队节点时,则不允许其获得锁,而是让其也加入同步队列中进行排队。
- 使用tryAquireShared的时候,如果成功,且当前线程在队列中,那么将会依次唤醒后续shared节点。后续shared节点线程将会将前面获取成功的节点出队列,并将自己置为头结点,直到最后一个连续的shared节点。
CONDITION
跟OBJECT的notify一样一样的用法。但比其功能更强大,能设立多个CONDITION,根据场景NOTIFY不同队列的线程。进入WATING首先要获得锁,然后进入CONDITION的WATING队列,然后释放锁。被NOTIFY后会进入 同步队列 ,等待获取之前的锁,然后继续运行。
一些锁的实现思路
TWINS LOCK
只允许两个线程获得锁的不可重入锁
因为是两个的线程都可获得锁,因此用AQS的SHARE相关方法会比较合适。虽然在一个线程释放锁,就唤醒另外一个锁的场景用 独占形式的方法 也可以。但AQS的SHARE形式比较合适。
将可获得锁的线程个数参数化为 N(TWINS则N=2),将status初始化为N,表示还有N个资源可被获取。
tryAcquireShared伪代码如下:
int tryAcquire(int acquire){
for(;;){//循环CAS是因为有可能同时两个线程都在竞争锁,且两个都有可能竞争成功,为了节约进入队列进行等待的消耗,因此采用循环CAS
int status = getStatus();
int newStatus = status - acquire;
if(status > 0 && compareAndSetStatus(status,newStatus))
return newStatus;//大于0表示可唤醒排队中的线程尝试获取同步状态,等于0表示无需通知后续等待线程
else
return newStatus;//小于0表示获取失败
}
}
tryReleaseSHared伪代码如下:
boolean tryRelease(int release){
for(;;){//存在同时释放锁的场景
int status = getStatus();
int newStatus = status + release;
if(compareAndSetStatus(status,newStatus)){
return true;
}
}
}
ReentranceLock
可重入锁。排它,但锁拥有者可重入。
实现思路,通过重写 tryAcquire,tryRelease 实现。
//本实现为非公平锁实现。若要实现公平锁,则需要检测自己是否队列第二个节点,若是,才尝试去获取锁。
pubulic boolean tryAcquire(int acquire){
int status = getStatus();
if(status != 0){
//检测自己是否OWNER
if(Thread.currentThread() == getExclusiveThread()){
setStatus(status + acquire);
return true;
} else {
return false;
}
}else{
if(compareAndSetStatus(0,acquire)){
setExclusiveThread(Thread.currentThread());
return true;
}else{
return false;
}
}
}
public boolean tryRelease(int release){
if(Thread.currentThread() != getExclusiveThread()){
throw new IllegalAccessException();
}
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> newStatus = getStatus() - release;
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(newStatus < <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">throw</span> <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">new</span> OverFlowException();
}<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span> {
setStatus(newStatus);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(newStatus == <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>){
setExclusiveThread(null);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-literal" style="box-sizing: border-box;">true</span>;
}<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span>{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-literal" style="box-sizing: border-box;">false</span>;
}
}
}
ReentranceReadWriteLock
可重入,读锁共享,写锁独占,写锁可降级读锁,可区分公平非公平实现。
实现思路: 利用status高16位表示读锁的总重入次数,低16位表示写锁的重入次数。 读锁中每个线程的重入次数使用各自的ThreadLocal记录,staus记录的是总次数。写锁获取者只有一个,无需类似的ThreadLocal变量。
boolean tryAcquire(int acquire){
int status = getStatus();
int writeStatus = getWriteStatus(status);
int readStatus = getReadStatus(status);
if(writeStatus != 0){
if(getExclusiveThread() = Thread.currentThread()){
setStatus(calcStatusFromWR(writeStatus+acquire,readStatus))
return true;
}else{
return false;
}
}else{
if(readStatus == 0){
if(isSecondNodeMatter() && compareAndSetStatus(0,calcStatusFromWR(acquire,0)) ){//isSecondNodeMatter方法用于保证公平性
setExcusiveThread(Thread.currentThread());
return true;
}else{
return false;
}
}else{
retun false;
}
}
}
int tryAcquireShared(int acquire){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">for</span>(;;){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> status = getStatus();
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> writeStatus = getWriteStatus(status);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> readStatus = getReadStatus(status);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(writeStatus != <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(Tread.currentThread() == getExclusiveThread()){
setStatus(calcStatusFromWR(writeStatus,readStatus + acquire));
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>;<span class="hljs-comment" style="box-sizing: border-box; color: #999988; font-style: italic;">//acquire success but not propogation</span>
}<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span>{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> -<span class="hljs-number" style="box-sizing: border-box; color: teal;">1</span>;<span class="hljs-comment" style="box-sizing: border-box; color: #999988; font-style: italic;">//can't acquire</span>
}
}<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span>{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(isSecondNodeMatter() && compareAndSetStatus(status,calcStatusFromWR(<span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>,readStatus + acquire))){
threadLocalAcquire.add(acquire);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-number" style="box-sizing: border-box; color: teal;">1</span>;
}
}
}
}
boolean tryRelease(int release){
int status = getStatus();
int writeStatus = getWriteStatus(status);
int readStatus = getReadStatus(status);
int newWriteStatus = writeStatus + release;
if(newWriteStatus == 0){
setExclusiveThread(null);
setStatus(calcStatusFromWR(newStatus,readStatus));
return true;
}else{
setStatus(calcStatusFromWR(newStatus,readStatus));
return false;
}
}
boolean tryReleaseShared(int release){
int threadLocalAcquired = this.threadLocalAcquired.get();
if(threadLocalAcquired <= 0){
throw new RuntimeException();
}
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> newLocalAcquired = threadLocalAcquired - release;
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(newLOcalAcquired < <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">throw</span> <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">new</span> RuntimeException();
} <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span> <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(newLocalAcquired > <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">for</span>(;;){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> status = getStatus();
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> writeStatus = getWriteStatus(status);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> readStatus = getReadStatus(status);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(writeStatus > <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>){
setStatus(calcStatusFormWR(writeStatus,readStatus - released));
} <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span> {
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(!compareAndSetStatus(status,calcStatusFormWR(writeStatus,readStatus - released))){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">continue</span>;
}
}
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">this</span>.threadLocalAcquired.<span class="hljs-built_in" style="box-sizing: border-box; color: #0086b3;">set</span>(newLocalAcquired);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-literal" style="box-sizing: border-box;">false</span>;
}
} <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span>{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">for</span>(;;){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> status = getStatus();
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> writeStatus = getWriteStatus(status);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(writeStatus > <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>){
setStatus(calcStatusFormWR(writeStatus,readStatus - released));
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">this</span>.threadLocalAcquired.<span class="hljs-built_in" style="box-sizing: border-box; color: #0086b3;">set</span>(newLocalAcquired);
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-literal" style="box-sizing: border-box;">false</span>;
} <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span> {
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">int</span> newReadStatus = readStatus - released;
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(compareAndSetStatus(status,calcStatusFormWR(writeStatus,newReadStatus))){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">if</span>(newReadStatus = <span class="hljs-number" style="box-sizing: border-box; color: teal;">0</span>){
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-literal" style="box-sizing: border-box;">true</span>;
}<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">else</span>{
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-literal" style="box-sizing: border-box;">false</span>;
}
}
}
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">return</span> <span class="hljs-literal" style="box-sizing: border-box;">false</span>;
}
}
}
JAVA并发容器和框架
ConcurrentHashMap
HashMap非线程安全,HashTable效率低下
ConcurrentHashMap使用锁分段技术降低锁竞争。具体实现类似于(不太恰当)一个ConcurentHashMap由多个HashMap组成,每个HashMap都对应于一段HashCode对应的KEY,PUT进去的时候需要获得跟HashMap对应的锁。
ConcurrentLinkedQueue
FIFO队列 使用CAS入队及出队。
核心优化技巧是,tail指针(volatile变量),head指针(volatile变量)都允许不是指向队列第一个节点/最后一个结点,允许其与第一个节点/最后一个结点的距离小于等于HOPS变量(默认为1)。这么做的原因是volatile写的消耗是远远大于volatile读的。而如果要保持 tail永远指向最后一个结点,那么需要写两个volatile变量(tail变量 及 node.next,head的情况也类似),因此设计了允许 head不指向第一个节点,而是通过链表向前查找的形式查找第一个节点的算法。
阻塞队列
一个队列能够阻塞地插入(队列满则阻塞等待)及阻塞地移除(队列空则阻塞地等待) 那么它就是阻塞队列。
阻塞队列的 入队出队 共有四组方法。这四组方法在队列不可用时提供不同的响应,如:抛出异常,返回特殊值,一直阻塞,超时退出。
JDK的七个阻塞队列:
- ArrayBlockingQueue:数组结构有界阻塞队列
- LinkedBlockingQUeue:链表结构无界阻塞队列
- PriorityBlockingQueue:支持优先级的无界阻塞队列
- DelayQueue:一个使用优先队列实现的无界阻塞队列,其优先顺序根据元素距离执行时间的长短而排序。其可以用于实现 缓存定时过期/定时任务 等场景
- SynchronousQueue:一个不保存元素的队列,需要 生产者消费者 一起 生产/消费。本队列消费速度较快。
- LinkedTransferQueue:链表结构无界阻塞队列。与LinkedBlockingQueue的区别是,有tryTransfer方法,如果有消费者正在等待消费,那么生产者将会直接将消费元素传递给消费者
- LinkedBlockingDeque:链表结构无界双向阻塞队列
阻塞队列的主要实现原理:使用通知模式实现(即使用Condition,不符合条件则循环await)。阻塞队列会维护两个Condition(两个等待队列):full,empty.队列满后,生产者执行put操作时就会被阻塞然后在等待队列中等待。当消费者取出元素后,notify full队列中的元素,让其加入同步队列,若获取同步状态成功,则可以put数据到queue中。当队列为空,需要消费时的情况与此类似。
Fork/Join框架
分拆任务多线程计算,再合并返回结果的框架。
JAVA中的13个原子操作类
原子更新基本类型
- AtomicBoolean
- AtomicInteger
- AtomicLong
原子更新数组
- AtomicLongArray
- AtomicIntegerArray
- AtomicReferenceArray
原子更新引用类型
- AtomicReference
- AtomicReferenceFieldUpdater
- AtomicMarkableReference ?? 不太理解....
原子更新字段类
- AtomicIntegerFieldUpdater
- ... 用于原子更新已有的字段。
并发工具类
CountDownLatch
等待倒数完成,所有等待的线程都可以执行。不可重用。
CyclicBarrier
等待指定个数的线程进入await阶段后,一起开始执行。
Semaphore
允许指定数量的资源,资源被消耗完后,只能阻塞等待。重入也会减少资源数
Exchanger
两个线程交换数据后继续执行
线程池
- coreThreadCount:当线程池中的线程小于coreThreadCount的话,无论有没有空闲线程,都会创建新的线程
- maxThreadCount:当等待队列已满时,就会新增线程处理提交的任务,如果总的线程数已经达到了maxThreadCount,那么就会按照设定的策略(abort,discard,callerRuns,discardOldestQueueNode)处理无法执行的任务。当线程空闲下来时,会将线程关闭到coreThreadCount个。如果队列是无界队列,那么maxThreadCount这个参数无效。
shutdown/shutdownNow方法
- 共同点:拒绝接受新的任务。
- 不同点:第一个只会对没有执行任务的线程发出中断请求,第二个会对所有运行中线程发出中断请求。
建议使用有界队列,这样能及早发现性能问题,并便于排查。
Executor框架
- Executor接口
- 里面只有一个方法 void execute(Runnable task)
- 接口解耦(decoupling)了 任务的提交 与 任务的运行调度
- 主要的运用场景类似 将一个Runnable实例提交到Executor里执行,而非手动创建线程,然后调用Thread.start()
- Future接口
- 表征一个异步执行的结果
- 通过get方法可以阻塞地获得执行结构
- 可以调用Cancel方法尝试终止未完成的任务
- 传入参数false,表示若任务已开始执行的话就不尝试中断,让任务继续执行,但是任务的状态变成 Canceled,isDone返回true
- 传入参数true,表示任务已经开始执行则发送中断命令,并将任务状态置为已中断,isDone返回true
- cancel成功 并不代表 任务执行过程退出了,只是该任务的返回将会被忽略
- 方法isDone
- 其表征是否已经执行完成。
- 对于Cancel方法执行成功时,不管 任务是否继续执行中
- 方法isCanceled
- 当任务执行完成前被成功执行cancel方法,那么isCanceled返回true.
- RunnableFuture
- 继承了Runnable接口及Future接口的一个接口
- FutureTask类
- RunnableFuture的一个实现
- 可以将Runnable实例或者Callable实例封装成RunableFuture。
- 因实现了Runable接口,其可以用Executor调度,也可以直接新建线程调度,也可以在同一个线程内调度
- ExecutorService接口
- 继承自Executor接口
- 里面新增了两类功能
- 关闭Executor服务相关的方法
- shutdown():执行后,拒绝执行接收新任务,但已经在队列中的任务会继续处理,当线程没有任务执行时,执行中断操作,通知其退出。
- shutdownNow():与shutdown()类似,只不过不再执行已经在队列中但尚未开始的任务,无论线程有没有在执行任务,都会对其发出中断请求,请求其退出。
- isShutdown():是否调用了shutdown方法
- isTerminate():调用shutdown方法后是否所有的线程都已结束任务。
- awaitTerminate():等待所有线程执行完毕
- 新增监控任务运行状态及结果的方法(通过返回的future方法)
- Future<T> submit()
- List<Future<T>> invokeAll()
- 关闭Executor服务相关的方法
- AbstractExecutorService
- 故名思义,其为ExecutorService实现了一个骨架类,提供了invokeAny,invokeAll,将Runnable封装成Callable等操作
- 抽象一些公用的部分到这里
- ThreadPoolExecutor
- ExecutorService的一个重要实现类,继承自AbstractExecutorService
- JDK建议使用Executors的工厂方法创建该类的实例
- Delayed接口
- 实现了Comparable接口,使其可以在 优先级队列中进行排序
- 包含方法getDelay,用于获取本对象距离到时时间还有多长的时间
- ScheduledFuture
- Mix-in Delayed 及 Future 两个接口
- 其为ScheduledExecutorService一些有返回值得异步方法的返回结果的表征
- RunnableScheduledFuture接口
- MIX-IN了RunnableFuture接口及ScheluedFuture
- 该接口实例的run方法会将执行结果放入到Future的相关输出中
- 该方法还提供一个方法isPeriodic,表示本任务是否需要周期执行,若是,则将其再放入等待执行的队列中
- ScheduledExecutorService接口
- 扩展自ExecutorService
- 新增一些任务的定时调度功能,如
- 指定时间后,执行某个task
- 每隔一段间隔,执行重复执行某个task
- ScheduledThreadPoolExecutor类
- 是ScheduledExecutorService接口的主要实现类,但是JDK建议不直接使用本类,而是使用Executors的工厂方法来创建
- 使用一个优先队列来维护需要执行的任务的先后顺序(DelayQUeue,内部包含优先级队列,且当队列头结点的到时时间到才能从中取出节点。)
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于