文章目录
- 0、原子性、可见性、有序性以及如何实现
- JVM中如何保证原子性可见性有序性
- happen-before的原则是什么
- volitale关键字
- synchronized关键字
- volatile和synchronized有什么区别
- ThreadLocal原理分析和使用场景
- 无锁CAS与Unsafe类及其并发包Atomic
- Lock与ReentrantLock可重入锁
- 线程的状态有哪些?
- 2、并发级别由哪些
- 4、创建线程的几种方式
- 5、线程基本操作与线程协作
- 9、多线程锁的优化
- 10CAS(不加锁结合CopyOnwrite讲讲);确保原子性
- 11、JUC并发包的队列和原子类
- 12、简述线程池(非常重要)
- 13 快速失败和安全失败
- 15、ConcurrentHashMap原理
- 16、讲一下CopyOnwrite思想(不加锁)可以结合乐观锁CAS思想
- 16、Java中死锁
- 17、wait方法为什么定义在Object中以及哪些方法会释放锁?
- 18、单例模式
- 19、生产者与消费者模式
- 20、说一下并发安全的容器?
- JAVA多线程之线程间的通信方式
0、原子性、可见性、有序性以及如何实现
原子性
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。
可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。
JVM中如何保证原子性可见性有序性
保证原子性
- 除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性
- 可以使用
原子类
保证可见性
可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见
保证有序性
以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化
happen-before的原则是什么
倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下
定义了哪些指令不能重排
单线程的happen-before原则
:在同一个线程中,书写在前面的操作happen-bvefore后面的操作锁的happen-before原则
: 解锁必然发生在随后的枷锁(lock)之前、- 1voliatle规则1:volatile变量的写,先发生于读,这保证了volatile变量的可见性
传递性
:A先于B,B先于C,则A比先于C线程的start()方法先于它的每一个动作
线程的中断先于被中断线程的代码
线程的所有操作先于线程的终结
对象创建的happen-before原则:一个对象的初始化完成先于它的finalize方法调用
volitale关键字
内存语义
- 保证被volatile修饰的共享gong’x变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。
- 禁止指令重排序优化。
可以保正可见性
.有序性
但是不能保证原子性
为什么不能保证原子性
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
原理:内存屏障
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量。volatile变量正是通过这种写-读方式实现对其他线程可见
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
使用例子
DCL:单例模式
/**
* Created by zejian on 2017/6/11.
* Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
*/
public class DoubleCheckLock {
private static DoubleCheckLock instance;
private DoubleCheckLock(){}
public static DoubleCheckLock getInstance(){
//第一次检测
if (instance==null){
//同步
synchronized (DoubleCheckLock.class){
if (instance == null){
//多线程环境下可能会出现问题的地方
instance = new DoubleCheckLock();
}
}
}
return instance;
}
}
synchronized关键字
简述一下
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行
在JDK早期版本中,synchronized属于重量级锁,效率低下,因为监事器锁是依赖于底层的操作系统的
Mutewx Lock来是爱心的,Java的线程是映射到操作系统的原生线程之上的
。如果想要挂起或者唤醒一个线程,都需要操左系统帮忙,而操作系统实现线程之间的切换时需要从用户态切换到内核态,这个状态之间的转换需要相对比较长的时间,这也是为什么早期synchronized效率低的原因,庆幸的是在Java6 之后,JVM对synchronized做了大量的优化,比如:自旋锁,适应性自旋锁,锁消除,锁粗化,锁偏向。轻量级锁等技术来减少锁操作的开销
如何使用
主要有三种使用方式
- 同步方法块: 对给定对象加锁,进入同步代码前需要获得给定对象的锁
- 同步方法: 锁是当前实例对象
- 静态同步方法:锁是自己定义的锁对象
具体使用:单例模式
懒汉式
public class Test{ private volitakle Test instance; //对象变量引用 //构造函数私有化 private Test(){} //这种存在的问题是锁的粒度太大 public synchronized Test getInstacne{ if(instance==null){ instance = new Test(); } return instance; } }
DCL
public class Test{ private volitale Test instance; //对象变量引用 //构造函数私有化 private Test(){} public Test getInstacne{ //先判断一下对象是否已已经实例化过,没有实例化过才进入加锁到吗 if(instance==null){ //对类对象进行加锁 synchronized(Test.class){ if(instance==null){ instance = new Test(); } } } return instance; } }
另外,需要注意 采用
volitale
关键字修饰也是很重要的为什么
Test test = new Test()
这段代码其实是分三步执行:
- 分配内存空间
- 初始化unique
- 将test指向分配的内存地址
*但是由于JVM指令重拍的特性,执行顺序可能是1-3-2,指令重拍在单线程的环境下不会出现问题,但是在多线程黄江下会获得还没有初始化的实例。例如,线程T1先执行1和3,此时T2调用getInstance()后发现test不为空,因此返回对象,但是此时test还没被初始化
使用volitale可以禁止JVM的指令重拍,保证在多线程环境下也能找那个长运行
底层原理
实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现
具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。
对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁
核心对象头中的监视器Monitor
Java对象头的monitor是实现synchronized的基础!,锁的就是monitor;对象头内保存着无锁状态,偏向锁、轻量级锁、重量级锁等待状态位和标志为;
monitor
来标志一个对象的锁定状态(线程和对象之间的锁定关系)(在Java对象头中,存在一个monitor对象,每个对象自创建之后在对象头中就含有monitor对象,monitor是线程私有的,不同的对象monitor自然也是不同的,因此对象作为锁的本质是对象头中的monitor对象作为了锁。这便是为什么Java的任意对象都可以作为锁的原因。。)
- Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针) Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
- `所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁
- 在Java虚拟机(HotSpot)中,
monitor是由ObjectMonitor
实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp
文件,C++实现的)`
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet;每个等待锁的线程都会被封装成ObjectQaiter对象
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
同步代码块与同步方法底层原理
同步代码块底层原理:使用monitotrnmt和monitorexi实现:
monitorenter
指令插入到同步代码块的开始位置,monitorexit
指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor(在对象头中)与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
**同步方法底层原理:方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放
可重入性
重入锁实现可重入性原理或机制是:,
每一个锁关联一个线程持有者和计数器
,当技术器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一个线程请求成功后,JVM会记录下锁的持有线程,并且将计数器职位1,此时其他线程请求该锁的时候,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器递增;当想爱你成退出同步代码块时,技术器会递减,如果技术器为0,则释放该锁锁的优化
偏向锁:判断是否为偏向锁。直接判断当前线程id,减少同步时间
轻量级锁:
自旋锁
重量级锁
锁的消除技术
JDK16对synchronize关键字锁的优化
偏向锁
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间
线程中断与synbchronized
事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效
等待唤醒与synchronized
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,
·wait主动释放锁
在JDK中体现
StringBuffer
HashTable
volatile和synchronized有什么区别
出题:多线程对一个volitale变量进行++,最终这个变量结果准确吗?
答案:不准确,因为volatile不保证原子性,最终结果是小于真实值的。
如何保证原子性:
- synchronized
- 原子类 :采用CAS机制
volitale
- voltaile本质是在告诉jvm当亲变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问这个变量,其他线程被阻塞
- volitale仅能使用在变量级别,synchronized则可以使用在变量和方法上
- volitale仅能实现变量的修改可见性(缓存数据不对问题),原子性保证不了;而synchronzied则可以保证变量的修改可见性和原子性
- volatile不会造成线程阻塞,而Synchronized会造成线程阻塞
ThreadLocal原理分析和使用场景
什么是ThreadLocal变量
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
原理
在ThreadLocal内有
ThreadLocalMap·
内部类,一切操作都是围绕这个内部类展开的
ThreeadLocalMap
的Key为虚引用
,key为threadLocal
自身
因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
return (T)map.get(this);
// Maps are constructed lazily. if the map for this thread
// doesn't exist, create it, with this ThreadLocal and its
// initial value as its only entry.
T value = initialValue();
createMap(t, value);
return value;
}
内存泄露问题
使用场景
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
存储用户session
存储数据库连接
无锁CAS与Unsafe类及其并发包Atomic
原子性
原子操作意为“不可被中断的一个或一系列操作”。再多处理器上实现原子操作就变的有点复杂。
加锁是一种悲观的策略,它总是认为每次访问共享资源的时候,总会发生冲突,所以宁愿牺牲性能(时间)来保证数据安全。
无锁是一种乐观的策略,它假设线程访问共享资源不会发生冲突,所以不需要加锁,因此线程将不断执行,不需要停止。一旦碰到冲突,就重试当前操作直到没有冲突。
无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
CAS的思想很简单:三个参数,一个当前内存值V,旧的预期值A,即将更新的值B,当且晋档预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false
无锁的执行者: CAS
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其他原子操作都是利用类似的特性完成的。而整个JUC都是建立在CAS之上的,因此对于
synchronized
阻塞算法,JUC在性能上有了很大的提升
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作
javaCAS的实现-Unsafe类
在JAVA中,
sun.misc.Unsafe
类提供了硬件级别的原子操作来实现这个CAS
。java.util.concurrent
包下的大量类都使用了这个 Unsafe.java 类的CAS操作;其内部方法操作可以像C的指针一样直接操作内存;
CAS是一些CPU直接支持的指令,也就是我们前面分析的无锁操作,在Java中无锁操作CAS基于以下3个方法实现,在稍后讲解Atomic系列内部方法是基于下述方法的实现的。
//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
CAS的典型应用—atomic包
java.util.concurrent.atomic
包下的类大多是使用CAS操作来实现的(eg.AtomicInteger.java,AtomicBoolean,AtomicLong
)。下面以AtomicInteger.java
的部分实现来大致讲解下这些原子类的实现。
它提供了原子自增方法、原子自减方法以及原子赋值方法等
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;// 初始int大小
// 省略了部分代码...
// 带参数构造函数,可设置初始int大小
public AtomicInteger(int initialValue) {
value = initialValue;
}
// 不带参数构造函数,初始int大小为0
public AtomicInteger() {
}
// 获取当前值
public final int get() {
return value;
}
// 设置值为 newValue
public final void set(int newValue) {
value = newValue;
}
//返回旧值,并设置新值为 newValue
public final int getAndSet(int newValue) {
/**
* 这里使用for循环不断通过CAS操作来设置新值
* CAS实现和加锁实现的关系有点类似乐观锁和悲观锁的关系
* */
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
// 原子的设置新值为update, expect为期望的当前的值
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 获取当前值current,并设置新值为current+1
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
// 此处省略部分代码,余下的代码大致实现原理都是类似的
}
一般来说在竞争不是特别激烈的时候,使用该包下的
原子操作性能比使用 synchronized 关键字的方式高效的多(查看getAndSet()
,可知如果资源竞争十分激烈的话,这个for循环可能换持续很久都不能成功跳出。不过这种情况可能需要考虑降低资源竞争才是)。
在较多的场景我们都可能会使用到这些原子类操作。一个典型应用就是计数了,在多线程的情况下需要考虑线程安全问题
。通常第一映像可能就是:
public class Counter {
///这个版本是线程安全的
private int count;
public Counter(){}
public int getCount(){
return count;
}
public void increase(){
count++;
}
}
上面这个类在多线程环境下会有线程安全问题,要解决这个问题最简单的方式可能就是通过加锁的方式,调整如下:
public class Counter {
private int count;
public Counter(){}
public synchronized int getCount(){
return count;
}
public synchronized void increase(){
count++;
}
}
这类似于悲观锁的实现,我需要获取这个资源,那么我就给他加锁,别的线程都无法访问该资源,直到我操作完后释放对该资源的锁。我们知道,悲观锁的效率是不如乐观锁的,上面说了Atomic下的原子类的实现是类似乐观锁的,效率会比使用 synchronized 关系字高,推荐使用这种方式,实现如下:
public class Counter {
private AtomicInteger count = new AtomicInteger();
public Counter(){}
public int getCount(){
return count.get();
}
public void increase(){
count.getAndIncrement();
}
}
ABA问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到他仍然是A值,那么我们就能说她的值没有被其他线程改变过吗
如果在这段期间它的值曾经该成了B,后来又改成了A,那么CAS操作就会无人为它没有被改变过,这个漏洞称为ABA问题。解决的核心思想是加上时间戳来标志不同阶端的数值。
比如JUC包为了解决这个问题,提供了一个带有标记的原子引用婆娘个类“AtmociSampedReference”,它可以通过控制变量值的版本来保证CAS的正确性,如果需要解决ABA问题,改用传统的互斥同步(典型的就是synchronized和Lock)可能会比原子类更高效
CAS实现自旋锁
CAS的应用----concurrent包的实现
由于java的CAS同时具有
volitale
读和volitale
写的语义,因此Java线程之间的通信方式有下面四种方式:
A线程写volatile变量,随后B线程读这个volatile变量。
A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量
voliatle
变量的读写和CAS可以晒心啊线程之间的通信,把这些特型整合在一起,就形成了concurrent
包得以实现的基石,如果我们仔细分析concurrent
包的源代码实现,会发现一个通用化的实现模式 :
- 首先,声明共享变量为volitale
- 然后,使用CAS的原子条件更新实现线程之间的同步
- 同时,配合以`volitale的读/写和CAS锁具有的volitale读和写的内存语义来实现线程之间的通信
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下
Lock与ReentrantLock可重入锁
Lock锁接口
public interface Lock {
//加锁
void lock();
//解锁
void unlock();
//可中断获取锁,与lock()不同之处在于可响应中断操作,即在获
//取锁的过程中可中断,注意synchronized在获取锁时是不可中断的
void lockInterruptibly() throws InterruptedException;
//尝试非阻塞获取锁,调用该方法后立即返回结果,如果能够获取则返回true,否则返回false
boolean tryLock();
//根据传入的时间段获取锁,在指定时间内没有获取锁则返回false,如果在指定时间内当前线程未被中并断获取到锁则返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//获取等待通知组件,该组件与当前锁绑定,当前线程只有获得了锁
//才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
Condition newCondition();
可见
Lock
对象锁还提供了synchronized
所不具备的其他同步特性,如可中断锁的获取(synchronized在等待获取锁时是不可中的)
,超时中断锁的获取
,等待唤醒机制的多条件变量Condition
等,这也使得Lock锁在使用上具有更大的灵活性。
重入锁ReetrantLock
入锁ReetrantLock,JDK 1.5新增的类,实现了Lock接口,作用与synchronized关键字相当,但比synchronized更加灵活。
ReetrantLock本身也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,同时也支持公平锁与非公平锁。所谓的公平与非公平指的是在请求先后顺序上,先对锁进行请求的就一定先获取到锁,那么这就是公平锁,反之,如果对于锁的获取并没有时间上的先后顺序,如后请求的线程可能先获取到锁,这就是非公平锁,一般而言非,非公平锁机制的效率往往会胜过公平锁的机制,
//查询当前线程保持此锁的次数。
int getHoldCount()
//返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner();
//返回一个 collection,它包含可能正等待获取此锁的线程,其内部维持一个队列,这点稍后会分析。
protected Collection<Thread> getQueuedThreads();
//返回正等待获取此锁的线程估计数。
int getQueueLength();
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition);
//返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition);
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread);
//查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads();
//查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition);
//如果此锁的公平设置为 true,则返回 true。
boolean isFair()
//查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
//查询此锁是否由任意线程保持。
boolean isLocked()
ReentranLock原理:并发基础组件AQS与ReetrantLock
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现
都依赖于它,如常用 的`ReentranLock/Semaphore/CountDownLatch…
AbstractQueuedSynchronizer
又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int
类型的成员变量state
来控制同步状态,当state=0
时,则说明没有任何线程占有共享资源的锁,当state=1
时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,AQS内部通过内部类Node
构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject
构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。
同步队列
/**
* AQS抽象类
*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
//指向同步队列队头
private transient volatile Node head;
//指向同步的队尾
private transient volatile Node tail;
//同步状态,0代表锁未被占用,1代表锁已被占用
private volatile int state;
//省略其他代码......
}
head和tail分别是AQS中的变量,其中head指向同步队列的头部,注意head为空结点,不存储信息。而tail则是同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。state变量则是代表同步状态,执行当线程调用lock方法进行加锁后,如果此时state的值为0,则说明当前线程可以获取到锁(在本篇文章中,锁和同步状态代表同一个意思),同时将state设置为1,表示获取成功。如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待。其中Node结点是对每一个访问同步代码的线程的封装,从图中的Node的数据结构也可看出,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,Node是AQS的内部类
AQS作为基础组件,对于锁的实现存在两种不同的模式,即
共享模式(如Semaphore)
和独占模式(如ReetrantLock)
,无论是共享模式还是独占模式的实现类,其内部都是基于AQS实现的,也都维持着一个虚拟的同步队列
,当请求锁的线程超过现有模式的限制时,会将线程包装成Node结点并将线程当前必要的信息存储到node结点中,然后加入同步队列
等会获取锁,而这系列操作都有AQS协助我们完成,这也是作为基础组件的原因,无论是Semaphore还是ReetrantLock,其内部绝大多数方法都是间接调用AQS完成的
AQS的使用
*
CountDownLatch
- ·
ReentrantLock
Semaphore
ReentanLock与AQS关系
ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync,其中Sync继承自AQS实现了解锁tryRelease()方法,而NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()方法,ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。AQS提供了魔板,具体实现在各自的实现类中进行实现
基于ReetrantLock分析AQS独占模式实现过程
ReentrantLock中非公平锁
AQS
同步器的实现依赖于内部的同步队列(FIFO
的双向链表对列)完成对同步状态(state)的管理,当前线程获取锁(同步状态
)失败时,AQS会将该线程以及相关等待信息包装成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会将头结点head中的线程唤醒,让其尝试获取同步状态。关于同步队列和Node结点,前面我们已进行了较为详细的分析,这里重点分析一下获取同步状态和释放同步状态以及如何加入队列的具体操作,这里从ReetrantLock入手分析AQS的具体实现,我们先以非公平锁为例进行分析。
Condition
对应Object中的wait()
与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点
- 通过Condition能够精细的控制多线程的休眠与唤醒。
- 对于一个锁,我们可以为多个线程间建立不同的Condition。
public interface Condition {
/**
* 使当前线程进入等待状态直到被通知(signal)或中断
* 当其他线程调用singal()或singalAll()方法时,该线程将被唤醒
* 当其他线程调用interrupt()方法中断当前线程
* await()相当于synchronized等待唤醒机制中的wait()方法
*/
void await() throws InterruptedException;
//当前线程进入等待状态,直到被唤醒,该方法不响应中断要求
void awaitUninterruptibly();
//调用该方法,当前线程进入等待状态,直到被唤醒或被中断或超时
//其中nanosTimeout指的等待超时时间,单位纳秒
long awaitNanos(long nanosTimeout) throws InterruptedException;
//同awaitNanos,但可以指明时间单位
boolean await(long time, TimeUnit unit) throws InterruptedException;
//调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时
//间期限(deadline),如果没到指定时间就被唤醒,返回true,其他情况返回false
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须
//获取与Condition相关联的锁,功能与notify()相同
void signal();
//唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须
//获取与Condition相关联的锁,功能与notifyAll()相同
void signalAll();
}
关于Condition的实现类是AQS的内部类ConditionObject
ReentranLock与Synchronized的区别
在JDK 1.6之后,虚拟机对于synchronized关键字进行整体优化后,在性能上synchronized与ReentrantLock已没有明显差距,因此在使用选择上,需要根据场景而定,大部分情况下我们依然建议是synchronized关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。而ReentrantLock提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择ReentrantLock
锁的实现
synchronized是JVMmonitor对象实现的,而ReentranLock是JDK实现的
如何操作
sybchronized
会自动枷锁,释放锁,而ReebtranLock则需要手动加锁和释放锁
性能
新版本Java对synchronized进行了很多优化,如自旋锁等,synchronized与ReebtranLock大致相等
等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,该为处理其他事情
ReentranLock
可中断,Synchronized
不行公平锁
公平锁是值多个线程在等待桶一个锁时,必须按照申请锁的时间顺序来依次获得锁
Synchronized
中的锁是非公平的,ReebtranLock
默认情况下也是非公平的,但也可以是公平的。锁绑定多个条件
一个ReentranLock可以同时绑定多个Condition条件
都是可重入锁
线程的状态有哪些?
在Thread.state类中进行了定义
新建状态
表示刚刚创建的线程,这些线程还没有执行
可运行
可能正在运行,也饿能正在等待CPU时间片
阻塞
等待获取一个排他锁,其他线程释放了锁就会结束此状态
无线期等待
等待其他线程显示的唤醒,否则不会被分配CPU时间片
限期等待
:无须等待其他线程显示的唤醒,在一定时间之后会被系统自动唤醒
死亡
:可以是线程结束之后自己结束,或者产生了异常而结束
2、并发级别由哪些
阻塞·
无饥饿
无障碍
无锁
无等待
阻塞(BLOCKING)
一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用
synchronized
关键字,或者重入锁时就会产生阻塞的线程。无论是synchronized
或者重入锁
,都会视图在执行后需代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需要资源无饥饿(starvation-Free)
这个取决于线程之间是优先级的存在,如果系统允许高优先级的线程插队,这样有可能导致低优先级的线程产生饥饿
公平锁与非公平锁
无障碍
无障碍是一种最弱的非阻塞调度,相对来说非阻塞的调度是一种乐观策略
从这个策略可以看到,当临街区中存在严重冲突时,所有线程可能会不断的回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行
这也是利用了
CAS
理论的思想:一种可行的无障碍实现可依赖一个“一致性标记”来实现。线程在操作之前,先读取并保存这个标记,在操作完后才能够后,再次读取,检查这个标记是否被更改过,如果两者一致,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重式操作,而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全
无锁
无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,
无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区
,一个典型的特点是可能包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果修改成功,程序退出,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程是可以胜出的。至于临界区中竞争失败的线程,它则不断重试,直到自己获胜。如果总是尝试不成功,则会出现类似饥饿的现象,线程会停止不前5、无等待
无锁值要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进行扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。如果限制这个步骤上线,还可以进一步分解为有界无等待和线程数无关的无等待几种,他们之间的区别只是对循环次数的限制不同。一种典型的无扥带结构是RCU(rEAD-copy-update)。它的基本思想是,对数据的读可以不加控制。因此所有的读线程都是无等待的,他们既不会被锁定等待也不会引起任何冲突。但在写数据的时候,先取得原始数据的副本,接着只修改副本数据(这就是为什么读可以不加控制),修改完成后,在何时的实际回写数据
4、创建线程的几种方式
继承Thread类型创建线程
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
实现Runnable接口创建线程
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程
通过Future和Callable创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
使用线程池例如Excutor框架(工厂的方法)
- 创建线程池
- 创建线程池的三大方式 Executors的静态方法
- Executors.newSingleThreadExecutor()
- Executors.newFixedThreadPool(n)
- Executors.newCachedThreadPool()
- 自行创建线程池的七大参数 new ThreadPoolExecutor
- 执行WxecutorServices.execute(Runnable command)
创建线程的方式的对比
- 采用实现Runnable、callable接口的方式创建线程
线程了只是实现了Runnble接口或者Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开。但是,缺点是编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread
2、使用继承Thread类的方式创建多线程
如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可 获得当前线程。缺点是线程类已经继承了Thread类,就不能再继承其他父类了
3、Runnable和Callable的区别
- Callablle规定重写的方法是call(),Runnable规定(重写的方法)是run
- Callable的任务执行后可以返回值,而Runnavle的任务是不能返回值的
- call的方法可以抛出异常,run方法不可以
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果,它提供了检查计算是否完成的情况,可以取消任务的执行,还可获取执行结果
5、线程基本操作与线程协作
基本操作
Daemon
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程
sleep
Thread.sleep(millsec)方法会休眠当前正在执行的线程,millsec单位是毫秒(==抱着锁睡觉)
yield 礼让
对静态方法thread.yield()的调用声明了当前线程已经完成了声明周期最重要的部分,可以切换给其他线程来执行
join
一直阻塞当前线程直到目标线程执行完毕
start
启动线程
new
创建线程
线程协作
wait notify() 和notifyall() (工作在同步代码块中-)
使用
wait()
挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其他线程就无法进入对象 的同步方法或同步控制块中,那么就无法执行notify
或者notifyAll
来唤醒挂起的线程,造成死锁工作流程
- 一个线程调用了object.wait(),它就会进入object对象的等待队列;这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某个对象
- 当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程,并将其环形
- 方法不能随便调用,必须包含在对应的synchronized中
- 会自动释放锁
wait和sleep的区别
- 来自不同的类
- 关于锁的释放: wait会释放目标对象的锁,sleep不会释放任何资源
- 使用的范围的不同: wait必须工作在synchromnized中
3、await()、signal和singalAll()
- java.util.concurrent 类库中提供快乐Condition类来实现线程之间的协调
- 可以在Condition上调用await()来使得线程等待,其他线程调用singal()和singalAll()来唤性等待的线程
- 相比于wait这种等待方式,await可以指定等待的条件,因此更加灵活
class Data3{ // 资源类 Lock private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); private int number = 1; // 1A 2B 3C public void printA(){ lock.lock(); try { // 业务,判断-> 执行-> 通知 while (number!=1){ // 等待 condition1.await(); } System.out.println(Thread.currentThread().getName()+"=>AAAAAAA"); // 唤醒,唤醒指定的人,B number = 2; condition2.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printB(){ lock.lock(); try { // 业务,判断-> 执行-> 通知 while (number!=2){ condition2.await(); } System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB"); // 唤醒,唤醒指定的人,c number = 3; condition3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printC(){ lock.lock(); try { // 业务,判断-> 执行-> 通知 // 业务,判断-> 执行-> 通知 while (number!=3){ condition3.await(); } System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB"); // 唤醒,唤醒指定的人,c number = 1; condition1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
9、多线程锁的优化
减小锁持有时间
减小锁粒度(分隔数据结构),相对是锁的粗化
读写分离锁来替换独占锁(分割数据功能)(读和读之间不阻塞)
jvm虚拟机对锁的优化(这是synchronized优化)
偏向锁
第一个线程请求锁并拿到锁之后,下次在请求锁不需要进行同步操作,直解拿到锁。当有第二个线程竞争的时候,偏向锁失效
轻量级锁
> 锁偏向失效后,使用轻量级锁。将对象头部作为指针,指向锁记录,如果成功就获得轻量级锁,如果失败则升级为自旋锁
自旋锁
轻量级锁失效后,java虚拟机会假设在不久的时间内,可以拿到锁,所以让线程进行几个空循环,如果可以拿到锁则进入临街区,否则阻塞等待锁
锁消除
> 取出不能可能存在共享资源竞争的锁,节省不必要的锁的请求时间
10CAS(不加锁结合CopyOnwrite讲讲);确保原子性
11、JUC并发包的队列和原子类
阻塞队列
BlockingQueue(在线程池中可以用到)
java.util.courrent.BlockingQueue
接口有以下阻塞队列的实现,基于ReentranLock
FIFO队列 :LinkedBlockingQueue、ArrayBlockingQUeue(固定长度)
优先级队列:PriorityBlicungQueue用于实现最小堆和最大堆
阻塞队列提供了take()和put()方法,如果队列为空take()将阻塞,直到队列中有内容;如果队列满put将阻塞,直到队列有空位置
2、ArrayBlockingQueue和LinkedBlocking的区别
- 队列大小有所不同,ArrayBlocking是有解的初始化必须指定代销,而LinkedBlockiung可以是有界的也可以是无界的,对于后者而言,当添加速度大于移除速度的时,在无界的情况下,可能会造成内存溢出的情况
- 数据存储容器的不同:ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockinbgQueue采用的则是以Node节点做维护连接对象的链表
- 由于ArrayBlockingQUeue采用的是数组的存储容器,因此在插入或删除时不会产生或销毁任何额外的对象的实例,而LinkedBlockingQUeue则会生成一个额外的Node对象。这可能在长时间内需要高效的并发处理大批量数据时,对于GC可能存在较大的影响
- 两者的实现队列添加货已出的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加和溢出操作采用的是同一个ReentaranLock锁,而LinkedfBlockingqUEUE实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的是TakeLock没这样能大大提高队列的吞吐量,也意味着在高并发情况下生产者和消费者可以并行的操作队列中的数据,以此来提高整个队列的并发性能
自己如何实现阻塞队列(BlockingQueue)
就是实现这个JDK中的
ArrayBlockingQueue,其实就是生产者与消费者模式
- 核心Synchronized以及objectwait
- 当数组满时,让添加线程阻塞;当数组空时让取出线程等待
- 当数组有元素的时候,通知 等待在空信号线程运行
12、简述线程池(非常重要)
为什么要用线程池
降低资源的消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度:当任务到大时,任务可以不需要等待线程创建就能立即执行
方便管理:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控
JUC里面线程池代码体系
ThreadPoolExecutor
类实现了ExecutorServices
接口和Executor
接口,并由Executors
类扮演线程池工厂的角色
ThreadpOOLeXECUTOR
的七大参数
- corePoolSize:核心线程池大小(线程池维护线程的最小数量)
- maximumPoolSize:线程池维护线程最大数量
- keepAliveTime:超时了没有人调用就会释放
- unit: 线程池维护线程所允许的空闲时间单位
- workQueue: 线程池所使用的缓冲队列
- handler:线程池对拒绝任务的处理策略
常规实现的线程池(通过创建不同的ThreadPoolExecutor对象)
newFixedThreadPool 定长线程池
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
coreSize和maximumPoolSize都是用户设定的线程数量nthreads
keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停掉,但这里KeepALIVE无效
阻塞队列是一个无界队列,实际线程数量将永远维持着在nthreads,因此m,aximumPoolSize和keepAliveTime将无效
newCachedThreadPool可缓存
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
它是一个可以无限扩大的线程池;
它比较适合处理执行时间比较小的任务;
corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
keepAliveTime为60s,意味着线程空闲时间超过60S就会被杀死;
采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须找到一条工作线程来处理它,如果当前没有空闲的线程,那么就会再创建一条新的线程、
newSingleThreadExecutor单一线程
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
- 它指挥创建一条工作线程处理认为
讲述BlockingQueue
在线程池中的阻塞队列分为
直接提交队列
,有界队列
,无界队列
,优先级任务队列
直接提交队列
:设置为SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingQueue,它没有容量,每执行一个插入操作就会阻塞,需要在执行一个删除操作才会唤醒,反之每一个删除操作也都要等待对应的插入操作
有界的任务队列
:有界的任务对哦咧可以使用ArrayBlockingQueue实现。若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到CorePoolSize
时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue
初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSIZe,则执行拒绝策略
无界的任务队列
:无界任务队列可以使用`LinkedBlockingQueue实现。使用无界任务多了,线程池的任务队列可以无限制的添加新的任务,而线程池创建最大线程书刘昂就是你corePoolSize设置的数量,也就是说你设置的maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池数量达到corePoolSize后,就不会再增加了;若有后需的新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调和控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题
优先任务队列
:优先任务队列通过PriorityBlockingQueue
实现。通过运行的代码我们可以看出PriorityQuue
它其实就是一个特殊的无界队列,它其中无论添加多少个任务,线程池创建的线程数页不会超过corePoolSzie的数量,只不过其他任务队列一般是按照先进先出的规则处理任务,而PrioritYqUEWUE
队列可以自定义规则根据任务的优先级顺序先后执行
线程池的拒绝策略
- ThreadPoolExecutor.AbortPolicy():直接抛出异常,丢弃任务(当都满了)
- new ThreadPoolExecutor.CallerRunsPolicy() :不想抛弃执行任务,但是由于池中没有任何资源了,那么就会直接使用调用该
execute
的线程本身来执行,很有可能造成当前线程也被阻塞- new ThreadPoolExecutor.DiscardOldestPolicy():若程序执行尚未关闭,则位于工作队列头部的任务将被删除,然后重复执行程序(如果再次失败,则重复过程)
- ThreadPoolExecutor.DiscardPolicy():该策略默默的丢弃无法处理的任务,不予任何处理
线程池任务调度策略
当一个任务通过execute(Ruunabe)方法与添加到线程池时
- 若此时线程池中的数量小于
CorePoolSize
,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务- 若此时线程池的数量等于
CorePoolSize
,但是缓冲队列workQueue
未满,那么任务被放入到缓冲队列- 若此时线程池中的数量大于
CorePoolSize
,缓冲队列满,并且线程池中数量小于maximumPoolSize
,创建新的线程来处理被添加的任务- 若此时线程池中的数量大于
CorePoolSize
,缓冲队列workQuieue
满,并且线程池数量等于maximumPoolSize
,那么通过handler
锁指定的策略来处理任务也就是:
处理任务的优先级别为:核心线程
CorePoolSize
,任务队lie,最大线程数;若三者都满了,使用handler处理被决绝的策略当线程池中的线程数量大于
CorePOOLsIZE
时,若某线程空闲时间超过`KEEPALIVETIME·,线程将被终止。这样,线程池就可以动态的调整池中的线程数
13 快速失败和安全失败
快速失败
在使用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除和修改),则会抛出
Concurent Modification Exception
场景:
java.util
包下的集合类都是快速失败的,不能在多线程下发生并发修改(在迭代过程中被修改)安全失败
采用安全失败机制的集合容器,在遍历时不时直接在集合容器上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历
>
>原理
:由于迭代时对原集合的拷贝进行遍历,锁以在遍历过程中对原集合所做的修改不能被迭代器检测到,所以不会触发Concurrent Modification Exception
>
>缺点
:基于拷贝内容的优点避免了Concurrent Modification Exception
,但同杨的,迭代器并不能访问刀片修改后的内容,即迭代器遍历的是开始的那一刻拿到的集合拷贝哦,在遍历期间原集合发生的修改迭代器是不止到的
>
> 场景:java.util.concurrent
包下的容器都是安全失败的,可以在多线程下并发使用,并发修改
15、ConcurrentHashMap原理
16、讲一下CopyOnwrite思想(不加锁)可以结合乐观锁CAS思想
写入时复制思想
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
在java语言中,CopyOnwriteArrayList是Java中的并发容器类,同时也是符合写入时复制思想的CopyOnwrite容器
下面来看看,一段源码:
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();//获得锁
try {
Object[] elements = getArray();//得到目前容器数组的一个副本
E oldValue = get(elements, index);//获得index位置对应元素目前的值
if (oldValue != element) {
int len = elements.length;
//创建一个新的数组newElements,将elements复制过去
Object[] newElements = Arrays.copyOf(elements, len);
//将新数组中index位置的元素替换为element
newElements[index] = element;
//这一步是关键,作用是将容器中array的引用指向修改之后的数组,即newElements
setArray(newElements);
} else {
//index位置元素的值与element相等,故不对容器数组进行修改
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();//解除锁定
}
}
我们可以看到,在set方法中,我们首先是获得了当前数组的一个拷贝获得一个新的数组,然后在这个新的数组上完成我们想要的操作。当操作完成之后,再把原有数组的引用指向新的数组。并且在此过程中,我们只拥有一个事实不可变对象,即容器中的array。这样一来就很巧妙地体现了CopyOnWrite思想。
CopyOnWrite问题
- 只能保证数据的最终一致性,不能保证数据的实时一致性
- 存在内存占用的问题,因为每次对容器结构进行修改的时候都要对容器进行复制,这样一来我们就有了旧对象和新对象,会占用两份内存,如果对象占用的内存比较大,就会频繁的垃圾回收行为,降低性能
- 所以对于CopyOnwrite容器来说,只适合在读操作远远多于写操作的场景下使用,比如说
缓存
16、Java中死锁
死锁定义
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
竞争的资源可以是:锁
、网络连接
、通知事件
,磁盘
、带宽
,以及一切可以被称作“资源”的东西。
死锁出现原因
- 因为系统资源不足导致的资源竞争
- 进程运行推进顺序不合适:请求和释放资源顺序不当
- 系统资源分配不当
出现死锁四个必要条件(这也是怎么打破这个死锁的入手)
资源互斥
:一个资源只能被一个进程使用请求与保持
:当一个进程因请求资源而阻塞的时候,保持已获得资源不放不剥夺
:进程已获得资源,在未使用完成之前,不能被其他进程强行剥夺循环等待
:若干进程之间形成一种头尾相接的循环等待资源关系
死锁产生场景
互相持有对方想获得的锁
如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。
public static void main(String[] args) {
final Object a = new Object();
final Object b = new Object();
Thread threadA = new Thread(new Runnable() {
public void run() {
synchronized (a) {
try {
System.out.println("now i in threadA-locka");
Thread.sleep(1000l);
synchronized (b) {
System.out.println("now i in threadA-lockb");
}
} catch (Exception e) {
// ignore
}
}
}
});
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (b) {
try {
System.out.println("now i in threadB-lockb");
Thread.sleep(1000l);
synchronized (a) {
System.out.println("now i in threadB-locka");
}
} catch (Exception e) {
// ignore
}
}
}
});
threadA.start();
threadB.start();
}
线程池中的死锁
比如线程池中有一个线程可用,此时,任务A1依赖于任务2,任务1正在执行,但是任务2永源无法执行,因此造成死锁
如何解决(预防)死锁
死锁检测
Jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。
Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
Jconsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
以确定的顺序获得锁
针对两个特定的锁,开发者可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。
超时放弃
当使用
synchronized
关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock
接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException
方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
17、wait方法为什么定义在Object中以及哪些方法会释放锁?
所谓的释放锁资源实际是通知对象内置的monitor对象进行释放,而只有所有对象都有内置的monitor对象才能实现任何对象的锁资源都可以释放。
又因为所有类都继承自Object,所以wait()就成了Object方法,也就是通过wait()来通知对象内置的monitor对象释放,而且事实上因为这涉及对硬件底层的操作,所以wait()方法是native方法,底层是用C写的
。
而join()有资格释放资源其实是通过调用wait()来实现的
其他都是Thread所有,所以其他3个是没有资格释放资源的(除了这个方法Thread中的方法都不会释放锁
)
18、单例模式
面试的时候可能要手写单例模式,下面进行了总结;单例模式的核心是如何确保创建对象时是线程安全的
18.1 饿汉式
私有化构造器
类初始化时,立即加载对象
提供获取对象的方法
package GOF23.Singleton;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/3/1 0001 14:23
*
* 饿汉式单例
*
* 1、构造器私有化
* 2、上来创建一个静态对象
* 3、一个方法返回创建的静态对象
*/
public class SingleDemon01 {
//1、构造器私有化
private SingleDemon01(){
}
//思考为什么是静态对象?
//2、类初始化时,立即加载对象:
//这一步和JVM类记载有关,jvm保证静态变量在类加载的时候是线程安全的
//JVM类加载过程: 加载 验证 准备 解析 初始化
private static SingleDemon01 singleDemon01= new SingleDemon01();
//3、提供获取对象的方法,没有synchrionized,效率高
public static SingleDemon01 getInstance(){
return singleDemon01;
}
}
18.2、懒汉式
私有化构造器
类初始化不用立即加载对象
提供获取对象的方法,需要synchronized,效率低
package GOF23.Singleton;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/3/1 0001 14:33
* 饿汉式
*/
public class SingleDemon02 {
//1、构造器私有化
private SingleDemon02(){
}
//思考为什么是静态对象?
//2、类初始化时,不立即加载对象:
private static SingleDemon02 instance= null;
//3、提供获取对象的方法,有synchrionized,效率低
public static synchronized SingleDemon02 getInstance(){
if(instance==null){
instance = new SingleDemon02();
}
return instance;
}
}
18.3、DCL(double checking )
构造器私有化
类初始化是,不用立即加载对象
提供获取对象的方法,而不是在方法内部进行双重检测
为了避免指令重拍,可以采用volitale关键字
package GOF23.Singleton;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/3/1 0001 14:38
*
* 双重检测懒汉式:(DCL)_
*/
public class SingleDemon03 {
//1、构造器私有化
private SingleDemon03(){
}
//思考为什么是静态对象?
//2、类初始化时,不立即加载对象:
//为了避免指令重拍:可以在变量前加上 volitale关键字
private static volitale SingleDemon03 instance= null;
//3、提供获取对象的方法,双重检测
public static SingleDemon03 getInstance(){
if(instance==null){
synchronized (SingleDemon03.class) {
if(instance==null)
instance = new SingleDemon03();
}
}
return instance;
}
}
18.4、枚举单例(线程安全,调用效率高,不能延时加载)推荐
package GOF23.Singleton;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/3/1 0001 14:56
* 枚举
*/
public enum SingleDemon05 {
//枚举里纯天然就是单例的
INSTANCE;
public SingleDemon05 getInstance(){
return INSTANCE;
}
}
19、生产者与消费者模式
面试时,有可能要手写
生产这与消费者模式
- 可以使用原生的
Object.wait()与Object.notify
方法- 可以使用
ReentranLock生成的 Condition 进行Condition.await()与Condition.signal
方法- 在JUC包中,使用到了
ArrayBlockingQueue
也使用到了生产者与消费者模式
下面进行剖析
生产者消费者模式主要角色分析:生产者:用于提交用户请求,提取用户任务,并装入内存缓冲区
消费者:在内存缓冲区中提取并处理任务
内存缓冲区:缓存生产者提交的任务或数据,供消费者使用
任务:生产者向内存缓冲区中提交的数据结构
Main:使用生产者和消费者的客户端
生产者消费者编写的时候抓住:判断等待
,业务逻辑
,通知
下面以一个线程加1,一个线程减1的生产者消费者模式进行分析
19.1、Object方式实现生产者消费者模式
package com.zj.ProductAndConsumer;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/3/30 0030 20:57
*/
public class Case1 {
public static void main(String[] args) {
Data data1 = new Data();
new Thread(new Runnable() {
@Override
public void run() {
try {
while(true)
data1.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
while(true)
data1.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
// 判断等待,业务,通知
class Data{ // 数字 资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
while (number!=0){ //0
// 等待
this.wait();
}
//业务
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (number==0){ // 1
// 等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
this.notifyAll();
}
}
19.2、ReentranLock实现生产者与消费者
package com.zj.ProductAndConsumer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/3/30 0030 21:04
*/
public class Case2 {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
data.decrement();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
data.increatment();
}
}
}).start();
}
}
class Data2{
private int num = 0;
//用ReentranLock取代Synchronized
private ReentrantLock lock = new ReentrantLock();
//用Condition绑定这个lock
Condition condition = lock.newCondition();
//当数据为0的时候进行加1(业务逻辑判断)
//对数据加1
//通知
public void increatment() {
lock.lock();
//逻辑判断
try {
while(num==1){
condition.await();
}
//业务处理
num++;
System.out.println(Thread.currentThread().getName()+"--"+num);
//唤醒
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void decrement() {
//逻辑判断
lock.lock();
//逻辑判断
try {
while(num==0){
condition.await();
}
//业务处理
num--;
System.out.println(Thread.currentThread().getName()+"--"+num);
//唤醒
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
19.3、剖析ArrayBlockingQueue类
在ArrayBlockingQueue中使用生产者消费者模式,下面分析下
1`桶上卖,
可以看出在ArrayBlockingQuerue中定义了一些内部数据,
ReebtranLock,以及notEmpty和notFull条件Condition条件
下面来看一下,在其中的反方
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
//put阻塞等待
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //对put方法做同步
try {
while (count == items.length)//当前队列已经满了
notFull.await(); //等待队列有足够的空间
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal(); //通知非空线程有元素
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//对take方法做同步
try {
while (count == 0) //消费队列为空
notEmpty.await(); //则消费队列要等待一个非空的信号
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();//通知需要put()线程队列已经哟空闲空间(非慢的信号)
return x;
}
20、说一下并发安全的容器?
这种问题,就是分析JUC下面的类
最重要的是容器是如何实现并发安全的:底层原理
ConcurrentHashMap(自己整理过的)
在JDK7中采用分段锁来减少锁的竞争
JAVA8中放弃了分段锁,采用CAS(一种乐观锁),同时为了防止哈希严重冲突时退化成链表(冲突时会在该位置生成一个链表,哈希值相同的对象就链在一起),会在链表长度达到阈值(8)后转换为红黑树(比起链表,树的查询更稳定)
CopyOnWriteArrayList(并发版的ArrayList)
并发版本ArrayList,底层结构是数组,和ArrayList不同之处在于:当新增和删除元素会新建一个新的数组,在新的数组中增加或排除指定对象,最后用新增数组替换原来的数组
使用场景:由于读操作不加锁,写(增,删,改)操作加锁,因此适用于读多写少的场景。
局限:由于读的时候不会加锁,读取当前副本,因此可能会读取到脏数据,如果介意,建议不使用
CopyOnWriteSet并发Set
基于
CopyOnWriteArrayList
实现(内含一个CopyOnWriteList成员变量),也就是说底层是一个数组。
ConcurrentLinkedQueue 并发队列(基于链表)
基于链表实现的并发队列,使用
乐观锁(CAS)
保证线程安全。因为数据结构是链表,所以从理论上是没有队列大小限制的,也就是说添加数据一定成功
ConcurrentLinkeDeque并发队列(基于双向链表)
基于双向链表实现的并发队列,可以分别对头尾进行操作,因此除了先进先出(FIFO),也可以先进后出,当然先进后出;使用
CAS实现
ArrayBlockingQueue 阻塞队列
基于数组实现的可阻塞队列,构造时必须制定数组大小,往里面放东西时如果数组满了便会阻塞直到有位置(也支持直接返回和超时等待),通过一个
锁ReentrantLock
保证线程安全。
LinkedBlockingQueue 阻塞队列(基于链表)
基于链表实现的阻塞队列,想比与不阻塞的ConcurrentLinkedQueue,它多了一个容量限制,如果不设置默认为int最大值。
PriorityBlockingQueue 线程安全的优先队列
构造时可以传入一个比较器,可以看做放进去的元素会被排序,然后读取的时候按顺序消费。某些低优先级的元素可能长期无法被消费,因为不断有更高优先级的元素进来。
SynchronousQueue 数据同步交换的队列
一个虚假的队列,因为它实际上没有真正用于存储元素的空间,每个插入操作都必须有对应的取出操作,没取出时无法继续放入
JAVA多线程之线程间的通信方式
同步
:多个线程通过synchronized
关键字这种方式来实现线程间的通信wait、notify
机制Lock Condition
机制while轮询机制
:通过一些voliatale
变量,在一个线程不断查询是否满足了某各条件,若不满足某各条件则不断死循环
前面这几种是一种共享内存机制,由于轮询条件使用看了volitale关键字修饰时,这就表示他们通过判断这个共享的条件变量
是否改变了,来实现进程间的通信
管道通信
:使用java.io.PipedInputStream
和java.io.Pipedoutputstream
进行通信
更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个