基础知识
1.并发编程优缺点
1.为什么用到并发编程(优点)
- 充分利用多核CPU的运算能力
- 方便业务拆分,提升应用性能
2.并发编程的缺点
频繁的上下文切换
解决方法
- 无锁并发编程
- CAS算法
- 使用最少线程
- 协程
线程安全(常见的避免死锁的方法)
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout),当超时等待时当前线程不会阻塞
- 对于数据库锁,加锁和解锁必须在一个数据库链接里,否则会出现解锁失败的情况
3.容易混淆的概念
- 同步VS异步
- 并发VS并行
- 阻塞VS非阻塞
- 临界区资源
2.线程的状态和基本操作
1.如何新建线程
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- JDK中Executors工具类
几种新建线程的方式比较
2.线程状态转换
- new
- runnable
- waiting
- timed_waiting
- terminated
- blocked
3.线程的基本操作
interrupt
抛出InterruptedException时,会清除中断标志位
- interrupt()
- interrupted()
- isInterrupt()
sleep
sleep与wait的区别- sleep是Thread的静态方法,它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁
- wait是Object实例方法
- wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁
- sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行
join
等待调用join方法的线程运行结束,打个比喻:玩游戏时,谁join(参与)进来,就让他先玩结束,我们再继续玩
yield
yield仅仅只会把时间片让给同优先级的线程,而sleep没有这个要求
4.守护线程Deamon
守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的
5.线程的特点
- 原子性
- 可见性
- 有序性
JMM内存模型
java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量来完成隐式通信
线程安全问题原因
主内存和工作内存数据不一致
重排序
编译器指令重排序【编译器优化的重排序】
处理器指令重排序【指令级并行的重排序】【内存系统的重排序】
重排序解决方案
针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序
针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序
JMM内存模型
1.哪些是共享数据
- 实例域
- 静态域
- 数组
2.抽象结构
线程将数据拷贝到工作内存,再刷新到主存。各个线程通过主存中的数据来完成隐式通信
重排序
1.什么是重排序
为了提高执行性能,编译器和处理器会对指令进行重排序
编译器优化重排序->指令级并行重排序->内存系统重排序->最终执行的指令序列
针对编译器重排序,编译器重排序规则会禁止一些特定类型的编译器重排序
针对处理器重排序,编译器会在生成指令的时候插入内存屏障来禁止特定类型的处理器重排序
2.数据依赖性
编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序
3.as-if-serial【针对单线程】
遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按照程序的顺序来执行的
happens-before保证内存可见性规则【针对多线程】
1.定义
如果A happens-before B,则A操作的结果对B操作可见,且A操作在B操作之前执行
如果指令重排序之后的结果,与按照happens-before关系执行的结果一致,则指令可以重排序
2.理解
站在程序员角度:为编程人员提供了一个类似强内存的内存结构,方便编程。
站在编译器和处理器厂商角度:在不影响正确结果的前提下,可以让编译器和处理器厂商尽情优化
3.具体规则
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
- 传递性规则:如果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()操作成功返回
- 线程中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生
- 对象finnalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始
as-if-serial与happens-before对比
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度
synchronized
1.如何使用
- 实例方法:锁的是实例对象
- 静态方法:锁的是类对象,锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系
- 代码块根据配置,锁的可以是实例对象,也可以是类对象
2.moniter机制
字节码中会添加monitorenter和monitorexit指令
锁的重入性:同一个锁线程,不需要再次申请获取锁
每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一
3.synchronized的happens-before关系
线程A释放锁happens-before线程B加锁
4.内存语义
共享变量会刷新到主存中,线程每次会从主存中读取最新的值到自身的工作内存中
5.锁优化
锁状态
- 无锁状态
- 偏向锁
- 轻量级锁
- 重量级锁
CAS操作
是一种乐观锁策略,利用现在处理器的CMPXCHG指令
CAS存在问题
- 存在ABA问题
- 自旋时间可能过长的问题
- 只能保证一个共享变量的原子操作,解决方案:利用对象整合多个共享变量
java对象头
- 对象的hashcode 25bit
- 对象的分代年龄 4bit
- 是否是偏向锁的标志位 1bit
- 锁标志位 2bit
Synchronized VS CAS
元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。
6.锁升级策略
偏向锁轻量级锁
加锁:在对象头和栈帧记录中添加自身的线程ID
锁撤销:在全局安全点上进行
重量级锁
加锁:Displace mark word,对象头mark word通过CAS指向栈中锁记录
锁撤销:如果CAS替换回对象头失败,则升级为重量级锁
各种锁的比较
锁可以升级,但是不能降级
volatile
0.重点
是java虚拟机提供的轻量级同步机制,被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象
1.实现原理
写volatile变量在编译时添加Lock前缀指令
将当前处理器缓存行的数据写回系统内存
写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值
每个处理器会通过总线嗅探出自己的工作内存中的数据是否发生变化来保证缓存一致性【缓存一致性协议】
2.happens-before关系推导
3.内存语义
写volatile变量会重新刷新到主存中,其他线程读volatile变量会重新从主存中读取最新的值
4.内存语义的实现
通过在特定位置处插入内存屏障来防止重排序
- 1.在每个volatile写操作的前面插入一个StoreStore屏障
- 2.在每个volatile写操作的后面插入一个StoreLoad屏障
- 3.在每个volatile读操作的后面插入一个LoadLoad屏障
- 4.在每个volatile读操作的后面插入一个LoadStore屏障
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
屏障解释
- StoreStore屏障:禁止上面的普通写和下面的volatile写重排序
- StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
- LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
- LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
5.整体流程
- Lock前缀的指令会引起处理器缓存写回内存
- 一个处理器的缓存回写到内存会导致其他处理器的缓存失效
- 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值
6.三大特性总结
- 1.保证可见性
- 2.不保证原子性
- 3.通过禁止指令重排序保证有序性
final关键字
1.如何使用
变量
基本类型
- 类变量(static变量):只能在声明时赋值或者在静态代码块中赋值
- 实例变量:声明时赋值,构造器以及非静态代码块中赋值
- 局部变量:有且仅有一次赋值机会
引用类型
- final修饰的引用类型只保证引用的对象地址不变,器对象的属性是可以改变的
方法
被final修饰的方法不能被子类重写,但是可以被重载
类
被final修饰的类,不能被子类继承
2.final的重排序规则
final域为基本类型
- 禁止对final的写重排序到构造函数之外
- 禁止读对象的引用和读该对象包含的final域重排序
final域为引用类型
- 对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作时不能被重排序的
3.final实现原理
插入StoreStore和LoadLoad内存屏障
4.final引用不能从构造函数中“溢出”(this 逃逸)
java内存模型中定义的8种原子操作
- lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态
- unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用
- load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
- use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用
- write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
CAS
定义
CAS的全称是Compare-and-swap,他是一条CPU并发原语。他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
实现
基于JDK底层的Unsafe类实现。Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。
缺点
1.ABA问题:解决方案:原子更新类,添加版本号,AtomicStampedReference
2.循环时间长,性能消耗大
3.只能保证一个共享变量的原子操作,解决方法:将多个属性封装在一个对象中
Lock体系
1.Lock与synchronized的比较
Lock提供了基于API的可操作性,能提供可响应中断式获取锁,超时获取锁以及非阻塞式获取锁的特性
synchronized执行完同步块以及遇到异常会自动释放锁,而Lock需要显示的调用unlock方法释放锁
2.AQS
1.设计意图(模版方法设计模式)
- AQS提供给同步组件实现者,为其屏蔽了同步状态的管理,线程排队等底层操作。实现者只需要通过AQS提供的模版方法实现同步组件的语义即可。
- lock(同步组件)是面向使用者的,定义了接口,隐藏了实现细节
2.如何使用AQS实现自定义同步组件
- 重写protected方法,告诉AQS如何判断当前同步状态获取是否成功或者失败
- 同步组件调用AQS的模版方法,实现同步语义。而提供的模版方法又会调用被重写的方法
- 实现自定义同步组件时,推荐采用继承AQS的静态内部类
3.可重写的方法
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
4.AQS提供的模版方法
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中等待线程情况
3.AQS源码解析
1.AQS同步队列的数据结构
带头结点的双向链表实现的队列
2.独占式锁
- 同步状态获取成功则退出;失败则通过addWaiter方法将当前线程封装成节点加入同步队列,acqurieQueued方法使得当前线程等待获取同步状态
- 如果获取同步状态并且是同步队列中的头结点,则表明获取锁成功,并唤醒后继结点
- 可响应中断式获取锁以及超时获取锁特性的实现原理
3.共享式锁
- 锁获取原理
- 锁释放原理
- 可响应中断式获取锁以及超时获取锁特性的实现原理
4.ReentrantLock
1.重入锁的实现原理
判断state是否为0
- 0:直接获取锁,state设置为1
- 大于0:判断当前线程是否是获取锁的线程,若不是,获取锁失败;若是,state+1
2.公平锁的实现原理
锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO
3.非公平锁的实现原理
4.公平锁和非公平锁的比较
- 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象
- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量
5.ReentrantReadWriteLock
0.读线程允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞
1.如何表示读写状态的:低16位用来表示写状态,高16位用来表示读状态
2.WriteLock的获取与释放
- 当ReadLock已经被其他线程获取或者WriteLock被其他线程获取,当前线程获取WriteLock失败;否则获取成功。支持重入性
- WriteLock释放时将写状态通过CAS操作减一
3.ReadLock的获取和释放
- 当WriteLock已经被其他线程获取,ReadLock获取失败;否则获取成功。支持重入性
- 通过CAS操作将读状态减一
4.锁降级策略
按照WriteLock.lock()—>ReadLock.lock()—>WriteLock.unlock()顺序,WriteLock会降级为ReadLock,不支持锁升级
5.生产Condition等待队列
WriteLock可以通过newCondition方法生成Condition等待队列,而ReadLock无法生成Condition等待队列
6.应用场景
适用于读多写少的应用场景,比如缓存设计上
6.Condition机制
1.与Object的wait/notify机制相比具有的特性
Condition能够支持不响应中断,而Object不支持
Lock能够支持多个Condition等待队列,而Object只能支持一个
Condition能够支持设置超时时间的await,而Object不能
2.与Object的wait/notify相对应的方法
针对Object的wait方法:await,awaitNanos,…
针对Object的notify/notifyAll方法:signal,signalAll方法
3.底层数据结构
复用AQS的Node类,由不带头结点的链表实现的队列
4.await实现原理
将调用awai方法的线程封装成Node,尾插入到同步队列中,并通过LockSupport.park方法将当前线程设置为WAITING状态,直至其他线程通过signal/signalAll方法将其移入到同步队列中,使其有机会在同步队列中通过自旋获取到Lock,从而当前线程才能从await方法处退出。
5.signal与signalAll实现原理
将等待的队头结点移入到同步队列中
6.await与signal/signalAll的结合使用
7.LockSupport
1.主要功能
可阻塞线程以及唤醒线程,功能实现依赖于Unsafe类
2.与synchronized阻塞唤醒相比具有的特色
LockSupport通过LockSupport.unpark(thread)可以指定哪个线程被唤醒,而synchronized不能
ThreadLocal
1.实现思想
- 采用“空间换时间”的思想,每个线程拥有变量副本,达到隔离线程的目的,线程间不受影响解决线程安全的问题
- 操作系统
2.set方法原理
数据存放在由当前线程Thread维护的ThreadLocalMap中,数据结构为当前ThreadLocal实例的key,值为value的键值对
3.get方法原理
以当前ThreadLocal为键,从当前线程Thread维护的ThreadLocalMap中获取value
4.remove方法原理
从当前线程Thread维护的ThreadLocalMap中删除以当前ThreadLocal实例为键的键值对
5.ThreadLocalMap
底层数据结构
- 键为ThreadLocal实例,值为value的Entry数组
- 数组大小为2的幂次方
- 键ThreadLocal为弱引用
set方法原理
- 1.计算ThreadLocal的hashcode,总是加上ox61c88647,这是“Fibonacci Hashing”
- 2.计算待插入的索引为i,采用与运算
- 3.如何解决hash冲突,当索引为i处有Entry的话(hash冲突),就采用线性探测,进行环形搜索
- 4.加载因子,ThreadLocalMap初始大小为16,加载银子为2/3
- 5.扩容resize,容量为原数组大小的两倍
getEntry方法原理
根据ThreadLocal的hashcode进行定位,如果所定位的Entry的key与所查找的key相同则直接返回,否则,环形向后继续进行探测
rmove原理
先找到对应的entry,然后让它的key为null,之后再对其进行清理
6.ThreadLocal内存泄漏
造成内存泄漏的原因
由于ThreadLocal在Entry中是弱引用,当外部ThreadLocal实例被置为null后,根据可达性分析,堆中ThreadLocal不可达,会被GC掉,因此就存在key为null的entry。无法通过key为null去访问entry。因此,就会存在threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory引用链造成valueMemory不会被GC掉,造成内存泄漏
怎样来解决内存泄漏
- 关键方法cleanSomeSlots,expungeStaleEntry,replaceStaleEntry
- 在ThreadLocal的set,getEntry以及remove方法中都利用以上三个关键方法,对潜在的内存泄漏进行处理
为什么要使用弱引用
- 如果使用强引用的话,即使显示对ThreadLocal的实例置为null的话,由于Thread,ThreadLocal以及ThreadLocalMap引用链关系,ThreadLocal也不会被GC掉,反而会为程序员带来困扰
- 使用弱引用,尽管存在ThreadLocal内存泄漏的危险,但实际上已经对其进行了处理
7.ThreadLocal的最佳实践
使用完ThreadLocal后要remove掉
8.应用场景
- hibernate管理session,每个线程维护其自身的session,彼此不干扰
- 用于解决对象不能被多个线程共享的问题
线程池(Executor体系)
1.ThreadPoolExecutor
1.为什么要使用线程池
- 降低资源损耗
- 提升系统响应速度
- 提高线程的可管理性
2.执行流程
核心线程corePool,阻塞队列workQueue以及最大线程池maxPool三级缓存的工作方式
3.构造器各个参数的意义
- corePoolSize:核心线程池大小
- maxiumumPoolSize:线程池最大容量
- keepAliveTime:空闲线程可存活时间
- unit:keepAliveTime的时间单位
- workQueue:存放任务的阻塞队列【自定义时用什么队列】
- threadFactory:生产线程的工厂类
- handler:饱和丢弃策略
- AbortPolicy-队列满了丢任务抛出异常
- CallerRunsPolicy-如果添加到线程池失败,那么主线程会自己去执行该任务
- DiscardPolicy-队列满了丢任务不异常
- DiscardOldestPolicy-将最早进入队列的任务删,之后再尝试加入队列
4.如何关闭线程池
- shutdown:正在执行任务的线程,将任务执行完。空闲线程以中断的方式关闭
- shutdownNow:停止所有线程,包括正在执行任务的线程。返回未执行的任务列表
- isTerminated:来检查线程池是否已经关闭
shutdown:将线程池状态设置为SHUTDOWN,而shutdownNow将线程池状态设置为STOP
5.如何配置线程池
- CPU密集型:Ncpu+1
- IO密集型:2Ncpu
- 任务按照IO密集型和CPU密集型进行拆分
2.ScheduledThreadPoolExecutor
1.UML(类结构)
继承了ThreadPoolExecutor,并实现了ScheduledExecutorService
2.常用方法
- 可延时执行任务:schedule(…)
- 可周期执行任务:scheduledAtFixedRate(…)和scheduledWithFixedDelay
- scheduledAtFixedRate(…)和scheduledWithFixedDelay区别:FixedRate不要求任务结束了才开始统计延时时间,而WithFixedDelay要求从任务结束开始统计延时时间
3.ScheduledFutureTask
可周期执行的异步任务,每一次执行完后会重新设置任务下一次执行的任务,并且会添加到阻塞队列中
4.DelayedWorkQueue
按优先级排序的有界阻塞队列,底层数据结构是堆
3.FutureTask
1.FutureTask的几种状态
- 未启动(还未执行run方法)
- 已启动(已执行run方法)
- 已结束(正常结束,被取消,出现异常)
2.get()
未启动和已启动状态,get方法会阻塞当前线程直到异步任务执行结束
3.cancel()
- 未启动状态时,调用cancel方法后该异步任务永远不会再执行
- 已启动状态,调用cancel方法后根据参数是否中断当前执行任务的线程
- 已结束状态,调用cancel方法时会返回false
4.应用场景
当一个线程需要等到另一个任务执行结束后才能继续进行时,可以使用futureTask
5.实现了runnable接口
futureTask同样可以交由executor执行,获取直接调用run()方法
原子操作类
1.实现原理
借助于Unsafe类的CAS操作,达到并发安全的目的
2.原子更新基本类型
AtomicInteger,AtomicLong,AtomicBoolean
3.原子更新数组类型
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
4.原子更新引用类型
AtomicReference,AtomicReferenceFieldUpdater,AtomicMarkableReference
5.原子更新字段类型
AtomicIntegerFieldUpdater,AtomicLongUpdater,AtomicStampedReference
AtomicStampedReference可以解决ABA问题
并发工具
1.倒计时器CountDownLatch
- 当CountDownLatch维护的计数器减至为0的时候,调用await方法的线程才会继续往下执行,否则会阻塞等待
- 适用于一个线程需要等待其他多个线程执行结果的应用场景
- 操作系统
2.循环栅栏CyclicBarrier
当一组线程都达到了”零界点”时,所有的线程才能继续往前执行,否则阻塞等待
3.CountDownLatch与CyclicBarrier的比较
- 1.CyclicBarrier能够复用,而CountDownLatch维护的倒计数器不能复用
- 2.CyclicBarrier会在await处阻塞等待,而CountDownLatch在await处不会阻塞等待
- 3.CyclicBarrier提供了例如isBroken,getNumerWaiting等方法能够查询当前状态,而CountDownLatch提供的方法较少
4.资源访问控制Semaphore
适用于对特定资源需要控制能够并发访问资源的线程个数,需要先执行acquire方法获取许可证,如果获取成功后线程才能往下继续执行,否则只能阻塞等待;使用完后需要用release方法归还许可证。
5.数据交换Exchanger
为两个线程提供了一个同步点,当两个线程都达到了同步点之后就可以使用exchange方法互相交换数据;如果一个线程先达到了同步点,会在同步点阻塞等待直到另外一个线程也达到同步点
并发实践
生产者-消费者问题
1.使用Object的wait/notifyAll方式实现
使用Object的消息通知机制可能存在的问题
- notify过早,wait线程无法再获取到通知以至于一直阻塞等待。解决办法:添加状态标志
- wait条件变化。解决方法:使用while进行wait条件的判断,而不是在if中进行判断
- “假死”状态:使用notifyAll而不是notify
标准范式
- 永远在while中对wait条件进行判断,而不是在if中进行判断
- 使用notifyAll进行通知,而不要使用notify进行通知
2.使用lock的condition的await/signalAll的方式实现
3.使用blockingQueue方式实现
由于BlockingQueue有可阻塞的插入和删除数据的put和take方法因此,在实现上比使用Object和lock的方式更加简洁