多线程
2020-03-05 11:37:02 4 举报
AI智能生成
Java多线程知识点大全
作者其他创作
大纲/内容
JVM内存模型
工作内存&主内存<br>
java的多线程并发问题最终都会反映在JVM内存模型上,解决<font color="#c41230">原子性</font>、<font color="#c41230">可见性</font>、<font color="#c41230">有序性</font>3大问题<br>
主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)
Java内存模型规定所有变量都存储在<font color="#c41230">主内存</font>中,每条线程的<font color="#c41230">工作内存</font>使用到变量到主内存<font color="#c41230">副本</font>拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量<br>
所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的
线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成
JVM定义了线程对主存的操作原语
lock、unlock、read、load、use、assign、store、write<br>
可见性
串行语义(单线程)具有天生的可见性
根据可见性的原则,主内存的值可以被其他线程可见
锁释放之前,会将对变量的修改刷新到主内存,保证一个操作内一组原子操作的可见性
volatile保证了修饰的共享变量,当CPU发现这个指令时
将当前内核中线程工作内存中该共享变量刷新到主存
通知其他内核里缓存的该共享变量内存地址无效
有序性
一般情况下,编译器和CPU为了提升程序执行的效率,会按照一定的规则允许进行指令优化
happens-before先天原则
JVM的"happens-before"原则保证编译器进行优化时指令重排不会破坏原有的语义结构<br>
它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个规则,我们可以通过几条规则<br>一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。
CPU指令重排
代码编译后成二进制代码被加载到内存后按原语义有一套初始串行化指令
一个指令需要多个汇编步骤(多级流水线),每个步骤使用的硬件可能不一样
指令重排是减少流水线中断的一种技术,显著提高性能
指令重排可以保证串行语义一致,但是无法保证多线程之间的语义也一致,多线程之间是通过原语保证有序性
原子性
一个操作必须在一个线程执行,不可拆开,不可被中断,要么不执行要么执行完<br>
非原子性操作<br>
Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作。但是像j = i或者i++这样的操作都不是原子操作,<br>因为他们都进行了多次原子操作,两个原子操作加起来就不是原子操作<br>
32位操作系统对long、double类型的操作
原子指令(Java原语)
<font color="#c41230">原语</font>是指由若干条<font color="#c41230">机器指令</font>构成的,并用以完成特定功能的一段程序,这段程序在执行期间不可分割
CPU提供了在指令执行期间对<font color="#c41230">总线加锁</font>的手段,原子指令由硬件提供,供软件来实现,但是开销太大
CPU缓存一致性是比对总线加锁更高效的,MESI协议。java的CAS原理就是调用CPU的缓存一致性协议
非原子操作在并发访问时是线程非安全,需要使用并发处理策略实现<font color="#c41230">同步</font>操作,保证一个操作内一组原子操作的原子性<br>
如何保证3大特性
锁可以同时保证原子性、可见性、有序性3种特性,但是需要付出的性能代价最大<br>
volatile只能保证可见性和有序性,无法保证原子性,但是配合CAS就可以高效的完成功能<br>
线程管理
线程创建
继承Thread类
重写run方法
new Thread(task)
FutureTask装饰类
构造函数实现Callable接口<br>
任务有返回结果Future
FutureTask实现了Future接口和Runnable接口,即能作为Thread构造函数入参,也有返回结果
缺点是Future不支持回调,需要借助第三方包扩展回调处理能力,如:netty、guava<br>
Runnable接口
任务无返回结果,构造函数实现Runnable接口
lambda表达式
CompletableFuture类
特点
CompletableFuture是增强的Future,<font color="#c41230">支持回调</font>,模式类似于ES6中的<font color="#c41230">Promise</font>
如果不指定线程池,则默认使用默认线程池<font color="#c41230">ForkJoinPool </font>
初始方法
completedFuture()<br>
返回CompletableFuture,具有初始结果
runAsync()
返回CompletableFuture,任务无结果
supplyAsync()
返回CompletableFuture,任务有结果
allOf()
初始化一组CompletableFuture
中间方法
单个任务执行
thenApply(Function)
thenAccept(Consumer)
thenRun(Runnable)<br>
合并任务
runAfterBoth(Runnable)
thenAcceptBoth(BiConsumer)
thenCombine(BiFunction)
两个任务谁快计算谁
applyToEither(Function)
runAfterEither(Runnable)
处理异常
handle(BiFunction,exception)
whenComplete(BiConsumer,exception)
归约方法
get(long timeout, TimeUnit unit)
线程阻塞执行时间设置
complete()
手动结束线程
join()
@Async注解
配合completedFuture()可以返回一个CompletableFuture
返回void表示无返回结果
ThreadGroup线程组
线程与线程组之间的关系类似于文件与文件夹之间的关系,一个线程组可以包含多个线程以及其他线程组
一个线程组包含其他线程组的时候,该线程组被称为这些线程组的父线程组
给线程组起一个非常好听的名字非常重要
线程工厂
ThreadFactory.newThread方法中封装线程创建的逻辑和配置管理<br>
为线程关联 UncaughtExceptionHandler
为线程设置一个含义更加具体的有助于问题定位的名称、组信息<br>
确保线程的优先级为正常级别
线程创建的时候打印相关日志信息
线程池
ThreadPoolExecutor
corePoolSize
线程池维护线程的最少数量
workQueue
任务大于corePoolSize则优先放入该队列
maximumPoolSize
任务大于workQueue队列的容量,新建线程处理(线程池维护线程的最大数量)<br>
keepAliveTime
线程池中线程所允许的空闲时间,大于这个时间队列线程将会被kill<br>
unit<br>
线程池维护线程所允许的空闲时间的<font color="#c41230">单位</font>
handler<br>拒绝策略<br>
任务数大于maximumPoolSize,无法进行新建线程处理时,对这些任务的拒绝策略<br>
AbortPolicy(默认),直接抛出异常,阻止系统正常工作
CallerRunsPolicy
DiscardOldestPolicy,丢弃最老的请求
DiscardPolicy,默默丢弃无法处理的任务
合理的线程池大小
取得可用的CPU数量
Runtime.getRuntime().availableProcessors()
CPU密集型线程:Ncpu+1
I/O密集型线程:2×Ncpu<br>
这是因为I/O密集型线程在等待 I/O操作返回结果时是不占用处理器资源的
优先考虑将线程数设置为1,仅在一个线程不够用的情况下将线程数向2×Ncpu靠近
Exectors工具类
Executor 接口
ExecutorService 接口
CompletionService<br>
ScheduledExecutorService<br>
Executor接口方法
execute(Runnable)<br>
无返回值
submit(Runnable)
有返回值
Executors工具类方法
newFixedThreadPool
newSingleThreadExecutor
newCachedThreadPool
newScheduledThreadPool
spring的线程池技术
TaskExecutor接口
ThreadPool<font color="#c41230">Task</font>Executor
TaskScheduler接口
ThreadPoolTaskScheduler
@Scheduled
Fork/Join框架
Actor计算模型
临界区并发处理策略
阻塞
AQS(AbstractQueuedSynchronizer抽象类)<br>
抽象队列同步器AQS实现了对同步状态的管理、阻塞线程进行排队、等待通知等一些底层的实现处理,同时拥有 <font color="#c41230">同步队列</font> 与 <font color="#c41230">等待队列</font><br>
同步器状态的原子性管理<br>
原子状态使用CAS操作来存储当前锁的状态,判断锁是否被别的线程持有<br>
等待队列的管理<br>
所有没有请求到锁的线程都要进入等待队列等待<br>
线程阻塞与解除阻塞<br>
是阻塞<font color="#c41230">原语</font>park和unpark用来挂起和恢复线程<br>
线程阻塞和唤醒
阻塞
线程阻塞不占用CPU时间片,也不参与CPU上下文切换
线程阻塞会占用线程池的任务缓冲队列,新的任务进不来则被迫处于阻塞状态<br>
主动阻塞常用于延时执行
主动阻塞(等待)
主动释放锁,让出临界资源
Object对象
wait()
线程会进入该对象AQS等待队列中,等待队列中的线程不会主动去竞争该对象的锁
notify()
随机唤醒对象的等待队列中的一个线程,进入同步队列
notifyAll()
唤醒对象的等待队列中的所有线程,进入同步队列
Condition对象
LockSupport阻塞工具
Thread方法
当前线程
CurrentThread
返回的是对当前正在执行线程对象的引用
线程睡眠
sleep<br>
不释临界资源
线程中断
interrupt
并不是真是中断线程,而是打开中断标志位为true
interrupted
执行后自动将中断标志位清除为false<br>
线程插入
join
该方法是将调用方线程插入当前线程,当前线程阻塞直到join方法结束,从而控制线程的执行顺序
线程让步
yield
暂停放弃CPU资源,放弃CPU的时间不确定
被动阻塞
调用加锁的临界资源的方法,但是锁被其它线程占用,当前线程只能被动阻塞
调用阻塞方法
同步方法
加锁的方法
信号量方法
锁
锁的特点
原子性、可见性、有序性的前提保障
锁的开销
锁的申请和释放所产生的开销
锁可能导致线程频繁的上下文切换的开销
排他性&共享性
一个锁是否一次只能被一个线程持有,如:读写锁中读锁就是共享锁,写锁是排他锁(互斥锁)
公平&不公平
Fair
加锁前检查是否有排队等待的线程,优先排队等待的线程,<font color="#c41230">先来先得</font>
Nonfair
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
锁的分类<br>
内部锁(Synchronized关键字)
内部锁 synchronized 以前性能比Lock差一点,jvm优化后,性能差不多<br>
使用简单,易于维护,自动释放
相对功能比较单一
不是很复杂的功能前提下,建议使用内部锁 synchronized
Object阻塞方法
配合Synchronized搭档使用
常用方法
wait()
会让当前线程立即释放Object监视器,进入object对象的等待队列
notify()
随机唤醒等待队列中的线程,被唤醒的线程不是立即执行代码,而是先重新获取object监视器
显式锁<br>
Lock接口提供了一些内部锁所不具备的特性,但并不是内部锁的替代品<br>
ReentrantLock是一种标准的互斥锁,最多只有一个线程能拥有它。这样的实现是线程安全的,但是对读和写两种情况都进行了同步限制,那么在频繁读取时,会对性能造成不必要的浪费<br>
Lock方法<br>
lock<br>
获得锁,如果锁已经被占用,则等待<br>
tryLock<br>
尝试获得锁,如果成功返回true,失败返回false,该方法不等待,立即返回<br>
tryLock(time,unit)<br>
在<font color="#c41230">指定时间</font>内尝试获得锁<br>
unLock<br>
释放锁<br>
Condition对象阻塞<br>
配合Lock搭档使用<br>
常用方法<br>
await()<br>
要求线程释放锁,进入Condition对象的等待队列<br>
singal()<br>
从当前Condition对象的等待队列上随机唤醒一个线程,并且主动释放锁<br>
读写锁<br>
改进型的互斥锁,也被称为“共享/排他锁”<br>
读写锁允许多个线程可以同时读取(只读)共享变量,一次只允许一个线程对共享变量进行更新(包括读取后再更新) <br>
一个线程更新共享变量的时候,其他任何线程都无法访问该变量。<br>
读锁是可以同时被多个线程持有的,即读锁是共享的。任何一个线程持有一个读锁的时候,其他任何线程都无法获得相应锁的写锁。 <br>
写锁是排他的,即一个线程持有写锁的时候其他线程无法获得相应锁的写锁或读锁。 <br>
ReadWriteLock接口<br>
ReadWriteLock和显式锁Lock一样也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁<br>
常用方法<br>
readLock()<br>
获得读锁<br>
writeLock()<br>
获得写锁<br>
场景中使用<br>
只读操作比写(更新)操作要频繁得多。和volatile关键字功能类似,volatile适用于一个写,多个读<br>
读线程持有锁的时间比较长<br>
Semaphore信号量<br>
指定信号量(临界区)的准入数(线程数),如:临界区有10个资源则准入线程数最大只能是10,这样准入的线程数就不会发生阻塞<br>
计数信号量是对锁的扩展,无论是内部锁还是重入锁,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源<br>
加锁是为了安全而考虑,而限制线程并发数是为了性能考虑。Semaphore可以在构造方法中指定共享锁的数量,Semaphore在大于指定的共享锁数量时会阻塞
Semaphore方法<br>
acquire<br>
获得一个准入许可,如果无法获得,则等待<br>
tryAcquire<br>
尝试获得一个准入许可,如果成功返回true,失败返回false,该方法不等待,立即返回<br>
tryAcquire(<font color="#c41230">time</font>,unit)<br>
在<font color="#c41230">指定时间</font>内尝试获取准入许可<br>
release<br>
释放一个许可<br>
场景中使用<br>
流量控制,特别是对资源池的场景,比如数据库连接<br>
限制中的令牌桶算法<br>
线程阻塞<font color="#c41230">工具类</font><br>
LockSupport<br>
所有的方法都是静态方法
阻塞原语park和unpark的使用不会出现死锁的情况
可以让线程在任意位置阻塞
线程栈信息提示更加清晰,便于查到问题
TimeUnit
延时
sleep()<br>
常用颗粒度
TimeUnit.DAYS、TimeUnit.HOURS、TimeUnit.MINUTES、TimeUnit.SECONDS、TimeUnit.MILLISECONDS<br>
颗粒度转换
toMillis、toSeconds、toMinutes、toHours、toDays
并发控制实用<font color="#c41230">工具类</font>
CountDownLatch 倒计数控制顺序(对join的扩展),异步转同步
CycliBarrier 循环栅栏(对join的扩展)
<font color="#c41230">锁优化</font>标准
减小锁持有时间<br>
正则表达式的 Pattern 类,没有直接锁定方法,而是锁定方法中的一部分<br>
减小锁粒度<br>
ConcurrentHashMap<br>
锁分离<br>
读写锁<br>
ArrayBlockingQueue类不同方法的锁<br>
锁粗化<br>
不是将锁的粒度变大,而是合并锁请求<br>
锁消除<br>
锁消除是发生在编译器级别的一种锁优化方式,对不需要锁的位置进行自动锁消除<br>
<font color="#c41230">可重入</font>性<br>
一个线程在其持有一个锁的时候能再次(或者多次)申请该锁<br>
非常有利于提高锁性能,让轻量级锁成为可能<br>
JVM对锁的优化
锁竞争激烈程度<br>
锁偏向<br>
在几乎无竞争的条件下。当这个线程再次请求锁时,无须再做任何同步操作<br>
轻量级锁<br>
在轻度竞争的条件下,偏向失败就会进行轻量级加锁<br>
自旋锁<br>
轻量级锁加锁失败后,线程会<font color="#c41230">忙等待</font>,让当前线程做几个空循环(自旋),直到它拿到锁<br>
重量级锁(互斥锁)<br>
在竞争非常激烈的条件下,轻量级加锁且自旋都失败,锁请求就会膨胀为重量级锁(synchronized、Lock)<br>
互斥锁加锁失败后,线程会释放CPU被迫进入阻塞状态,操作系统内核会将线程置为wait状态,等到锁被释放后,内核会在合适的时机唤醒线程<br>
互斥锁加锁失败时,会用「线程切换」来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。<br>
常见锁问题
死锁
最常见的是数据库两个session对交叉数据的事务更新,权重较小的session将做出牺牲
非阻塞
随着硬件和操作系统指令集的发展和优化,产生了非阻塞同步,被称为<font color="#c41230">乐观锁</font>。简单地说,就是先进行操作,操作完成之后再判断操作是否成功,是否有并发问题,如果有则进行失败补偿,如果没有就算操作成功,这样就从根本上避免了同步锁的弊端
目前,在 Java中应用最广泛的非阻塞同步就是 CAS,在 IA64、 X86指令集中通过 cmpxchg指令完成 CAS功能,在 sparc - TSO中由case指令完成,在 ARM和 PowerPC架构下,需要使用一对 Idrex / strex指令完成。
无障碍
回滚重试
无锁
volatile
普通变量与volatile变量的区别是,volatile的特殊规则保证了写操作能立即同步到主内存,读操作立即从主内存刷新
volatile最适合使用的是一个线程写,其他线程读的场合
volatile配合CAS才能发挥其强大的无锁能力
局限性
无法保证原子性,volatile变量只有在<font color="#c41230">完整</font>的一个操作后才能主动写入内存<br>如:i++,这个非原子操作由3个原子操作组成,在操作过程中还未将i写入内存
CAS
全称 Compare And Swap,比较并交换。需要volatile变量配合才能发挥作用<br>
Unsafe类<br>
Unsafe类是进行底层操作的方法集合,可以直接操作内存,进行一些非常规操作,所以说是"不安全"的操作。<br>提供了硬件级别的原子操作,大多数API都是通过native函数实现,利用JNI来完成CPU指令的操作<br>
常用操作<br>
compareAndSwapObject()<br>
compareAndSwapInt()<br>
compareAndSwapLong()<br>
应用<br>
Atomic原子变量实现<br>
compareAndSet()<br>
Concurrent包实现<br>
A线程写volatile变量,随后B线程用CAS更新这个volatile变量<br>
A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量<br>
LongAdder<br>
JDK8推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观 锁的重试次数)<br>
无等待
无饥饿
RCU(Read-Copy-Update)
ThreadLocal<br>
线程上下文设计,用于实现线程内部的数据共享叫线程共享(对于同一个线程内部数据一致),即相同的一段代码多个线程来执行 ,每个线程使用的数据只与当前线程有关。
每个Thread维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类<br>
常用方法
set(T value)
从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。
get()
remove()
使用场景
替代request的attribute属性
并发容器类
基本原理
容器操作方法上设置阻塞(阻塞方法+锁优化)
容器上设置非阻塞(CAS)
CopyOnWrite
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentHashMap
ConcurrentLinkedQueue非阻塞队列
队列中元素按 FIFO 原则进行排序。没有任何锁操作,<font color="#c41230">完全采用 CAS操作</font>,来保证元素的一致性
BlockingQueue阻塞队列
生产者— 消费者模式,非常适合用于<font color="#c41230">数据共享的通道</font>
操作队列元素的方法都是<font color="#c41230">阻塞方法</font>
ArrayBlockingQueue<br>
基于数组实现的有界队列
LinkedBlockingQueue
基于链表实现的无界队列,默认是 Integer.MAX_VALUE
PriorityBlockingQueue
无界队列,有优先级
SynchronousQueue<br>
同步队列,该队列容量是0,严格说并不是一种容器,而是一个数据交换信道
每个put必须等待一个take,反之亦然。类似于生活中一手交钱一手交货,非常适合于传递性设计<br>
DelayedQueue
定时出队列,ThreadPoolTaskScheduler的底层实现
0 条评论
下一页