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