JAVA并发编程
2024-04-08 18:22:01 8 举报
AI智能生成
登录查看完整内容
为你推荐
查看更多
JAVA并发编程
作者其他创作
大纲/内容
JAVA中线程是执行程序的基本单位,而并发编程是同时执行多个线程以充分利用多核CPU性能
并发编程优点
并发编程缺点
基本概念
进程是一个在内存中运行的应用程序,它拥有自己独立的内存空间。线程是进程中执行任务的基本单元,一个进程至少有一个线程,可以运行多个线程,各个线程共享进程资源。
NEW 新建状态
REDAY 可被操作系统调度状态
RUNNING 运行状态
WAITING 等待状态
TIME_WAITING 超时等待状态
BLOCKING 阻塞状态
TERMINATE 终止状态
线程的生命状态
线程生命状态切换
线程
JMM内存模型是一个抽象概念,它定义了变量的访问方式。它在JVM中定义了主存和工作内存,每创建一个线程就定义一个工作内存,用来存储线程私有的数据。而主存中存储JAVA程序变量。所有线程共享主存中的变量。当一个线程访问主存中的数据时,会拷贝一个副本到工作内存,如果是基本类型就是变量的副本,如果是引用类型拷贝的是变量的引用。
JMM工作示意图
M(Modify修改状态)
E(Exclusive独占状态)
S(Shared共享状态)
I(Invalid无效状态)
MESI缓存一致性协议
JMM 内存模型
线程安全问题是指多个线程协同完成一个任务时,由于各个线程执行时间的不确定性,导致最终结果和预期结果不一致的问题。
问题描述
执行多个命令语句时,要么都执行成功要么都执行失败。
原子性
一个线程对共享资源的修改能够及时被其他线程感知到。
可见性
JVM在对程序编译时,出于对性能的考虑,在单线程执行且保证最终结果正确的情况下,对程序指令进行重排。但如果是多线程就不能保证程序的准确,所以多线程情况下需要程序上禁止指令重排,保证指令的有序性。
有序性
保证线程安全三要素
要判断程序什么时候需要做并发安全处理,只需要看在程序中是否有同一时间多个线程访问临界资源。
怎样判断程序需要保证并发安全
synchronized内置锁
AQS框架实现的Lock锁
使用juc包提供的并发容器,如ConcurrentHashMap、ThreadLocal等
Atomic原子类
JAVA中常用的保证线程安全实现
并发线程安全
一个线程修改了自己工作内存中的值,会被立即刷新到主存中。其他线程使用到该值时会重新从主存中读取。参考:MESI协议
保证可见性
编译器不再做指令重排优化
禁止指令重排
volatile作用
volatile关键字
JAVA提供的一种内置锁,它是一种对象锁,锁的是一个对象。用来解决并发安全问题。
介绍
锁对象是该方法的Class对象
加在类的静态方法上
锁对象是方法所在的实例
加在实例对象方法上
指定的锁对象lockObject
加在同步代码块上
使用
synchronized 获取锁/释放锁资源示意图
synchronized java内置锁,通过内部monitor(监视器)对象实现锁机制,线程进入monitor对象获取锁资源,线程退出monitor对象释放锁资源。monitor对象调用操作系统底层的mutex lock 互斥锁实现。mutex是一把重量级锁,涉及用户态和内核态的切换,是一个消耗性能的操作。在jdk1.6后对synchronized进行了优化,加入了偏向锁、轻量级锁、自旋机制。
Mark word 中包含 对象的 hashcode 、GC分代年龄、锁状态、是否偏向、偏向线程id、指向对象锁的指针
Mark word
指向该对象所属类的元数据指针,JVM中有关键性作用。如:类型识别、字段访问、方法查找等
klass pointer 指针
当为数组对象时,记录了数组的长度
数组长度(只有数组对象才有)
JAVA 对象头由三部分组成
Monitor可以看做是一种同步机制,在JAVA中被描述成一个对象,在hostop源码中是ObjectMonitor实现。在JAVA中每一个对象都有一个monitor锁,也就是synchronized锁。在对象的MarkWord头中保存着指向monitor锁的指针,指向着monitor对象的起始地址。
锁实现流程:多个线程进行锁资源竞争时
ObjectMonitor 类
Monitor(监视器)
字节码
示例类
经过上面的铺垫就不难理解monitorenter 表示进入同步代码块持有锁资源,monitorexit退出同步代码块释放锁资源
synchronized字节码层面实现
synchronized锁实现原理
无锁状态
偏向锁
轻量级锁
重量级锁
synchronized锁有四种状态
偏向锁:当同一个线程反复重入同步代码块时,此时锁进入偏向状态。
轻量级锁:多个线程执行同步代码块时,不存在资源竞争的情况,也就是多个线程交替执行,同一时间只有一个线程执行。此时锁状态为轻量级锁。
自旋优化:线程存在资源竞争,但竞争不激烈,也就是线程能很快获取锁资源,此时jdk 做了自旋优化,不直接进入重量级锁,先自旋尝试获取锁。
重量级锁:线程竞争非常激烈且JVM内部一系列自旋都失败的情况下,JVM会调用mutex重量级锁,此时锁状态为重量级锁。
升级过程
synchronized锁升级
原理
synchronized
Lock是JAVA JUC并发包下锁的顶层接口,它规定了锁实现的标准。如:Lock ()、unloc()加解锁等API
什么是Lock接口
AQS 在JAVA中是一个抽象类AbstractQueuedSynchronizer,它是JAVA JUC并发包下大多数同步器的一些列操作的高度抽象。如:ReentrantLocak、BlockQueue、CountDownLatch 等。
什么是AQS框架
state 在AQS中标志着是否可获取资源,在独占模式下state = 0 表示可获取资源,在共享模式下 通过线程对state的消耗判断是否可访问
state 资源
同步等待队列存放着竞争资源失败的线程
CLH 同步等待队列
在阻塞队列中,存放着被阻塞的线程
condition 条件等待队列
资源只能被一个线程独占,如ReentrantLock锁的实现
Node EXCLUSIVE独自模式
资源能被多个线程共享,如:CountDownLatch
Node SHARED 共享模式
AQS核心组件
AQS框架
ReentrankLock是JAVA JUC并发包下基于AQS实现的锁机制,它被大量运用在并发编程中,用来保证线程安全
什么是ReentrankLock
ReentrantLock 是一个class类,它只能作用在同步代码块上,切需要手动进行加解锁,否则会出现死锁问题。
ReentrantLock 使用示例
ReentrantLock 使用
都可用来保证线程安全
都是可重入锁
相同点
ReentrantLock 是JAVA juc并发包下的一个类,synchronized是JVM内置锁是一个关键字
ReentrantLock 可以是公平锁也可以是非公平锁,synchronized是非公平锁
ReentrantLock只能加在同步代码块上需要手动释放锁0,synchronized可以加在方法上,也可以加在指定对象上锁同步代码块。
ReentrantLock是可以中断的,synchronized不可中断。
不同点
ReentrantLock与synchronized 比较
没有获取到资源的线程将放入CLH同步等待队列阻塞,等待被唤醒
如果是获取到资源的线程反复进入同步代码块,每次state+1。实现可重入机制
lock()
获取到资源的线程退出同步代码块时,state-1 直到state = 0; 设置 Thread = null 。 唤醒CLH同步等待队列中的线程去竞争锁志愿
unlock()
ReentrantLock 实现原里
ReentrantLock
ReentrantLock可以保证线程安全,但是在读多写少的情况下。如果多个线程都是读取数据,并不会出现线程安全问题,但ReentrantLock都会进行加锁操作影响性能。所以有了ReentrantRedWriteLock 读写锁。在都是读的情况下不会进行加锁操作。
什么是ReentrantReadWriteLock读写锁
ReentrantReadWriteLock使用示例
ReentrantReadWriteLock使用类似,只不过在读操作上读锁,在写操作上加写锁
ReentrantReadWriteLock使用
ReentrantReadWriteLock
Lock
多个线程或进程在竞争资源或者通信过程中造成的一种阻塞现象,该阻塞在没有外力的作用下将无法推进下去,永久阻塞。
线程死锁示例
什么是死锁
线程在某一段时间类独占资源
互斥条件
线程请求到的资源在阻塞时,保持资源不释放
请求与保持条件
线程获取到的资源,在未使用完时不能被强行剥夺,只能自己完后释放
不可被剥夺条件
多个线程相互等待彼此所持有的资源
循环等待
死锁的四个必要条件
加锁时尽量加上超时时间
多个功能尽量避免使用同一把锁
加锁的粒度要小,降低同步代码块锁的范围
如何避免死锁
死锁
锁
ConcurrentHashMap 是并发条件下一种高性能HashMap容器,开发中如果在并发环境下需要使用到Map容器优先使用ConcurrentHashMap
什么是ConcurrentHashMap
ConcurrentHashMap是线程安全容器,HashMap是线程不安全容器
ConcurrentHashMap与HashTable 都是线程安全的,他们实现线程安全的方式不一样,HashTable 通过在方法上加synchronized 同步锁保证线程安全。ConcurrentHashMap通过 CAS操作+synchronized 实现线程安全。
ConcurrentHashMap与HashMap、HashTable的区别
CAS 保证线程安全
不存在哈希冲突:写数据时使用CAS操作保证线程安全
synchronized锁保证线程安全
存在Hash冲突:此时写数据会进行链表或者红黑树的遍历,使用synchronized锁保证写链表或者红黑树的线程安全
ConcurrentHashMap保证线程安全分为两种情况:存在Hash冲突和不存在Hash冲突的情况
ConcurrentHashMap如何高效的保证线程安全的
ConcurrentHashMap(jdk8)
CopyOnWriteArrayList 是 基于 写时复制机制实现的并发安全容器,对数据的写操作时,复值出底层数组副本,在新副本上进行操作,最后再覆盖旧数组。到达提高性能的目的
什么是CopyOnWriteArrayList
适用于读多写少的情况,读数据没有锁,提高并发性能
读写分离,减少并发冲突
优点
每次写数据都会复制副本,在高并发情况下,特别是底层数组比较大时,容易出现full gc 和 频繁复制副本带来的额外性能开销
数据一致性问题:CopyOnWriteArrayList 保证数据的最终一致性, 在某些时候不能保证数据一致,不适用于对数据一致性要求较高的场景
缺点
CopyOnWriteArrayList的优缺点
CopyOnWriteArrayList
什么是ThreadLocal?
在线程中使用了TreadLocal 没有及时调remove() 方法来清除它,那么ThreadLocal 变量将一直存在于内存中,只到线程被回收。如果线程是一个定时线程、或者一个守护线程永远不会被关闭,就 很有可能找出内存泄露。在使用ThreadLocal 后需要及时对ThreadLocal进行清理。代码开发中最好在finally 中对ThreadLocal 进行清除
ThreadLocal 使用示例:MySQL 连接管理
ThreadLocal 内存泄漏风险
ThreadLocal
阻塞队列除普通队列的特性先进先出外,额外加了两个特殊操作: 队列为空时,阻塞等待队列中有数据了才能执行读操作。队列满时需要等待队列可写(队列不满),才能往队列中放入数据。常用在生产者—消费者模型, 生产者写队列,消费者读队列。队列为满时消费者阻塞写,队列为空时生产者阻塞读。
什么是BlockingQueue
入队时,判断队列是否是满的,如果是满的调用notFull.await(); 将线程阻塞到notFull的条件等待队列,等待被消费者线程唤醒。如果队列未满,正常入队。最后调用notEmpty.signal(); 唤醒条件等待队列中的消费者线程
出队时,判断队列是否为空,如果是空的调用notEmpty.await(); 将线程阻塞到notEmpty的条件等待队列,等待被生产者线程唤醒,如果队列不为空,正常出队,最后调用notFull.signal();唤醒条件等待队列中的生产者线程
阻塞队列基于AQS框架列实现
BlockingQueue实现原理
一个基于数组的有界阻塞队列
ArrayBlockingQueue
一个基于链表的有界阻塞队列
LinkedBlockingQueue
一个支持优先级排序的无界阻塞队列
PriorityBlockingQueue
DelayQueue是一个延时无界阻塞队列,数据到期了才能被获取
DelayQueue
基于链表的无界阻塞队列
LinkedTransferQueue
基于链表的双向阻塞队列
LinkedBlockingDeque
常用BlockingQueue(阻塞队列)
BlockingQueue
并发容器
线程是执行程序的基本单位,而线程是一种稀缺资源,不能无限制的创建。大量创建线程会导致资源消耗巨大且系统稳定性降低。所以JAVA提供了线程池框架来维护线程。(线程池是一种线程的缓存技术,将线程缓存在一起做统一管理)
单个任务执行时间短
需要处理的任务量大
线程池使用场景
线程可重用
减少线程创建带来的性能开销
线程同一管理,提高可维护性
线程池优点
什么是线程池
RUNNING : 线程池处于正常运行状态
SHUTDOWNNOW: 该状态下线程不再接收新的任务,但会继续处理已提交的任务
STOP: 线程不会接收新的任务也不会执行已提交的任务
TIDYING: 所以任务都已处理完成,workCount= 0 ,调用行钩子方法 terminated()
TERMINATE :terminated()方法执行完成后处于 该状态,线程池正常关闭
线程池生命状态
execute(Runnable) : 提交线程任务,无返回值。
Executor是线程池的顶层接口,其中定义了一个方法execute()用来执行线程任务
shutdown() : 调用该接口时,停止接收提交的任务,但会继续执行已提交的任务
shutdownNow(): 停止接收提交的任务,也会终止正在执行的任务。
isShutdown(): 测试线程池状态,是否处于SHUTDOWN状态
isTerminated(): 测试线程池状态,是否处于TERMINATED状态。
submit(T ?): 提交线程任务,有返回值。
ExecutorService 是Executor 的一个重用补充接口,它定义了线程池的生命周期操作,如关闭线程池操作
ExecutorService有两个常用的线程池实现类。分别是ThreadPoolExecutor 默认线程池实现类和ScheduledExecutorService 定时线程实现类。
Executor
corePoolSize : 核心线程数,当核心线程为满时,提交任务优先创建核心新线程去执行
maximumPoolSize : 最大线程数,核心线程+ 非核心线程 。 当核心线程和阻塞队列满时,才去创建非核心线程执行任务。
keepAliveTime : 线程空闲最大存活时间,默认是非核心线程存活时间。设置allowCoreThreadTimeOut(true);允许核心线程超时关闭。
TimeUnit unit : 超时时间单位。
workQueue : 阻塞队列,核心线程达到设置值后再提交任务将放入阻塞队列,常用的阻塞队列有ArrayBlockingQueue/LinkedBlockingQueue/priorityBlockingQueuq/SynchronousQueue
threadFactory : 创建线程的工厂,默认使用defaultThreadFactory,可以重写线程工厂创建线程的方法实现自定义创建线程
AbortPolicy(默认): 抛出异常
DiscardOldestPolicy: 丢弃阻塞队列中队头的任务,将新任务放入队列中
- DicardPolicy: 直接丢弃新任务不做任何处理
- CallerRunsPolicy: 由调用者线程执行任务
也可以自定义拒绝策略
handler : 拒绝策略,当核心线程数,最大线程数、和阻塞队列任务都满时,在提交任务将走拒绝策略。默认是抛异常。线程池提供了四种拒绝策略
使用ThreadPoolExecutor创建线程池及参数说明
流程图
线程池任务提交流程: 创建核心线程处理任务——>核心线程数创建满时,将任务放入阻塞队列——>阻塞队列满时,创建非核心线程处理任务——>非核心线程数创建完时,走拒绝策略
1. 线程池中创建的线程在执行完任务后都阻塞在阻塞队列中
2. 非核心线程或核心线程的超时关闭,也是借助阻塞队列的超时机制实现
3. 实现线程的复用
阻塞队列在线程池中的作用
线程池使用代码示例
1.execute() 接收Runable 接口线程任务,没有返回值,不能拿到线程执行结果2. submit() 接收 Runable 接口任务,或者 Callable 类型的任务。返回Future对象,可以拿到线程执行结果
execute() 和submit() 的区别
ThreadPoolExecutor
ScheduledThreadPoolExecutor 是基于ThreadPoolExecutor实现的定时延迟池,用来执行循环定时任务或者延时任务。它没有非核心线程概念,不提供最大线程数参数。它的阻塞队列是自己实现的DelayedWorkQueue,不支持阻塞队列选择。
定时线程介绍
延时队列,这个队列是定时线程池里自己实现的一个延时等待队列。对入队的任务按照时间的顺序进行排队,越先入队的任务时间越靠前,当线程从队列中获取任务执行时,其获取的任务就是等待最久的任务。
DelayedWorkQueue
ScheduledThreadPoolExecutor(int corePoolSize) : 指定corePoolSize 执行任务的线程数
定时线程池的创建
三个提交任务核心API
用提交的任务创建ScheduledFutureTask任务,在ScheduledFutureTask设置延时时间,定时时间
将ScheduledFutureTask放入 DelayedWorkQueue 延时队列
创建工作线程Worker,从队列中获取任务。如果队列中的任务时间到期可以获取到任务。如果未到期阻塞线程
执行从队列中获取的任务,执行完成后重设定时时间
定时线程池定时实现原理(scheduleWithFixedDelay为例)
ScheduledThreadPoolExecutor
JAVA中默认的ThreadPoolExecutor线程池类参数非常多,如果对参数理解不到位很容易出问题。所以在juc并发包下提供了Executors 创建线程池的工具类,该类里面提供了一些静态工厂来创建常用的线程池。注:虽然Executors可以方便的创建线程池,但是也影藏了大量的创建细节,在不经意间的使用很可能导致性能降低,增加系统不稳定风险。推荐使用ThreadPoolExecutor创建线程池,理解ThreadPoolExecutor创建线程池的方式是很有必要的。
创建单个线程的线程池,只有一个线程执行任务
newSingleThreadExecutor
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小
newFixedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务
newCachedThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求
newScheduledThreadPool
Executors常用创建线程API
Executors
FutureTask是一个异步计算任务,它可以接收继承了Callable接口的任务。它可以对这个异步任务进行等待获取结果、判断是否已经完成、结束任务等操作。只有线程执行完成才能获取结果,否则会一直阻塞等待直到获取到结果。
什么是FutureTask
FutureTask JAVA代码使用示例
FutureTask
线程池
CountDownLatch可以看成一个控制并发的工具类,用于一个线程等待其他线程执行完成后,这个线程才恢复执行。
什么是CountDownLatch
CountDownLatch使用代码示例
CountDownLatch原理
CountDownLatch
CyclicBarrier 也是基于AQS 共享模式实现的线程通信工具,它的用法和 CountDownLatch相反。可以理解为是一个线程屏障,当一组线程到达CyclicBarrier 时会被阻塞,直到线程数达到CyclicBarrier 设置的预期阈值时,同时放行阻塞线程。
什么是CyclicBarrier
CyclicBarrier 使用代码示例
1. CyclicBarrier barrier = new CyclicBarrier(5); 初始化 栅栏屏障,线程阈值为52. barrier.await(); 添加屏障,现在到达该屏障会被阻塞,直到阻塞线程数到达初始化栅栏屏障阈值,放行线程
CyclicBarrier 原理
CyclicBarrier
Semaphore 信号量,也是基于AQS共享模式实现,它允许指定个数的多个线程同时访问资源。常用来作为并发限流操作。
什么是Semaphore
Semaphore JAVA代码使用示例
release(2)释放state状态 state += 2
Semaphore 原理
Semaphore
线程通信工具
CAS 比较与交换,在JAVA中是乐观锁的实现方式,即通过自旋非阻塞的方式不断尝试获取资源。在一定条件下CAS性能是优于直接加悲观锁的,CAS常常用来优化锁操作,如synchronized。
什么是CAS
CAS的操作通过对三个值修改实现。内存位置(V)、预期值(A)、新值(B) 。如果内存位置上的值和预期值一致,就将内存位置中的值修改为新值
CAS 实现原理
ABA问题: 线程 thread1 和线程 thread2 对内存中的值进行修改,它们的预期值都是 A , 线程 thread1 先将 值修改为了 B,但是很快又被它修改成了 A。此时Thread 2 再来修改时,是可以修改成功的。举个实际例子: A 到银行存了10万块钱,但是银行员工B私自挪用了这一笔钱用来炒股赚了20万,B把A的10万块还上了。A第二天查询余额10万没有变,A就认为钱没有问题,但实际中间是存在问题的,B涉嫌违法了。
JAVA中提供了解决ABA问题得到方案:AtomicStampedReference 原子类。通过新增一个版本号,每次更新值时版本号+1,只有当期望值和版本号都相等时才能正确的更新值。
ABA问题怎样解决
CAS存在的ABA问题
CAS 操作(compare and swap)
Atomic 是juc并发包下提供的无锁状态下解决并发问题的一种机制。它利用到了CAS 操作 和 Unsafe 的native 本地方法保证线程安全,主要用在少量临界资源同步的情况下,减少加锁带来的性能开销
AtomicInteger(基本类型)
AtomicIntegerArray(数组)
AtomicReference(引用)
AtomicStampedReference(引用代标记解决ABA问题)
AtomicIntegerFieldUpdater (更新字段)
Atomic 常用原子类(代码示例只展示怎么调用,不提供多线程情况演示)
Atomic 原子类介绍
JAVA并发编程
0 条评论
回复 删除
下一页