多线程大总结
2022-10-19 08:07:27 0 举报
AI智能生成
并发
作者其他创作
大纲/内容
阻塞队列(BlockingQueue)支持插入和移除等方法,一般用来实现生产者和消费者功能(生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。)
解决了什么?
ReentranLock + Condition 实现队列的阻塞,ReentranLock 是锁,Condition是条件状态,通过等待/通知机制,来实现线程之间的通信
图
实现方式
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
java有哪些阻塞队列
阻塞队列
字面意思是“可循环使用的屏障”。它的作用是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。
demo
CyclicBarrier 线程栅栏
CountDownLatch让一个或多个线程在运行过程中的某个时间点能停下来等待其他的一些线程完成某些任务后再继续运行。类似的任务可以使用线程的 join() 方法实现:在等待时间点调用其他线程的 join() 方法,当前线程就会等待join线程执行完之后才继续执行,但 CountDownLatch 实现更加简单,并且比 join 的功能更多。
作用
原理图
CountDownLatch
Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。
1、Semaphore 内部维护一组信号量,即一个 volatile 的整型 state 变量
2、Semaphore 分为公平或非公平两种方式,获取信号量或释放信号量的本质是对 state 进行原子的减少或增加操作
3、获取不到信号的线程放在等待队列里面,释放信号的时候会唤醒后继节点
4、Semaphore 主要用于对线程数量、公共资源(比如数据库连接池)等进行数量控制
总结
Semaphore 信号量
线程常见工具
负载因子 0.75
初始化值为16
链表树化为红黑树大于等于8,数组长度大于等于64
红黑树退化为链表因子为6
常见的变量
1,首先进入到putVal方法,然后判断table是否为空,为空则进行扩容2,不为空则通过扰动函数计算出key的hash值与(n-1)作与运算得到数组下标值3, 然后数组下标对应的位置是否有值,如果为空则直接插入,否则判断key值与hash是否相当,是的话直接覆盖4, 如果不相等的话,则判断是否是红黑树,如果是的话,则直接插入5,如果不是的话,判断链表长度是否大于8,如果小于8,则插入6,如果链表长度大于8,数组长度小于64,则就行扩容,而后转为红黑树,插入
put()方法流程
通过扰动函数(hashcode的高8位与低8位就行异或运算)得到一个hash值&(n-1)
怎么计算数组下标
1.减少碰撞次数, 2.增加查询效率, 3.减少空间浪费
为什么扩容为2的幂次方
当链表长度大于8的时候,而且数组长度大于64的时候,会转为红黑树当树的节点小于等于6个的时候,会退化成链表
什么时候树化,什么时候转会链表
不能自定义容量大小,比如传入6,也会扩容为8,为2的次幂方
时间复杂度为0(lgn)
1,数据结构加入了红黑叔
头插法主要存在的问题是:并发下调用transfer()方法,可能会导致链表死循环,以及数据的丢失。
代码
1.7中,插入链表节点使用头插法
1.8中变成了尾插法
2,链表插入节点的方式
Java1.8的hash()中,将hash值高位(前16位)参与到取模的运算中,使得计算结果的不确定性增强,降低发生哈希碰撞的概率。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
3,hash函数
JDK1.8之后做了什么优化?
HashMap
1 线程不安全的HashMap(在多线程环境下,使用HashMap 进行put操作会引起死循环,导致CPU利用率接近100%)2 效率低下的HashTable(HashTable 容器使用synchronized 来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下)3 ConcurrentHashMap 的锁 分段技术可有效提升并发访问率。
什么是CHM?
jdk1.7是对Segment就行加锁,加锁范围过大。采用的是lock+分段锁
jdk1.8是对node加锁,加锁粒度小,效率更高使用的是sync+cas保证
CHM如何解决线程安全问题
ConcurrentHashMap中维护着一个Segment数组,每个Segment可以看做是一个HashMap。而Segment本身继承了ReentrantLock,它本身就是一个锁。在Segment中通过HashEntry数组来维护其内部的hash表。每个HashEntry就代表了map中的一个K-V,用HashEntry可以组成一个链表结构,通过next字段引用到其下一个元素。
JDK1.7
去掉Segment的概念
JDK1.8
font color=\"#2196f3\
CHMJDK1.7&1.8的区别
CHM
HashMap&CHM
频繁创建,开销大不好管理(不知道哪里创建了线程、线程名字可能没有)
手动创建线程的缺点
1,可以提高性能,减少线程创建与销毁时带来的性能开销
2,对线程更好地进行管理,如线程数,存活时间等等
3,方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM, 并且会造成cpu过度切换(需要保持当前执行线程的现场,并恢复要执行线程的现场
为什么要有线程池?
主要为了实现线程的复用
核心线程是怎么做到不回收的?
有任务就处理,没有任务就阻塞(生产者-消费者)
设计思路
1、当提交任务的时候,如果工作线程小于核心线程,则创建线程,并执行任务
2、如果工作线程数大于核心线程数,且工作线程数小于最大线程数,则将任务丢入到阻塞队列里面
3、如果工作线程数小于最大线程数,且阻塞队列满了,则尝试创建临时线程去执行任务
4、如果工作线程数等于最大线程数,且阻塞队列满了,调用拒绝策略
主要流程
ThreadPoolExecutor
类
固定线程数的线程池
newFixedThreadPool
单一线程数的线程池
newSingleThreadExecutor
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newCachedThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
newScheduledThreadPool
jdk提供了哪些线程池
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明: Executors 返回的线程池对象的弊端如下:
主要目的就是不让cpu执行上下文切换
cpu核数+1
CPU密集型
主要是看IO等待时间
2*cpu核数+1
IO密集型
线程数量如何设置
核心线程数
最大线程数
存活时间
存活单位
拒绝策略
线程工厂
基本参数
谁提交任务,谁就负责执行任务(线程池不管)
CallerRunsPolicy
丢弃任务并抛出RejectedExecutionException异常。 【默认】
AbortPolicy
也是丢弃任务,但是不抛出异常。
DiscardPolicy
将最早进入队列的任务删除,之后再尝试加入队列
DiscardOldestPolicy
不接不管,自己去处理这封信
丢掉这封信,并说我不爱你
直接丢掉这封信,并不管你
将第一封信丢掉,然后再尝试接收你的信
联想对女孩子写表白信
线程池中的线程没有回收的过程,线程如果执行完毕,那么线程就会结束,当有任务时,核心线程会阻塞在任务队列的take方法,不会结束线程
核心线程也是可以回收的,只需设置allowCoreThreadTimeOut=true
线程池中,底层启动线程是调用Worker中thread的start方法,执行任务是通过调用阻塞队列中Runnable的run方法
线程池中的maximumPoolSize参数,代表当我任务队列满了的时候,可以添加除主线程外的线程来帮忙执行任务,但是核心线程加额外的线程不能超过maximumPoolSize的值
注意事项:
线程池
1,更好的压榨CPU资源,提示程序运行效率
2, 提高并发量
为什么要用线程?
可以从java编译到.class,再到类加载,到jvm运行来讲,被jvm加载的程序,活着的程序就叫进程
线程:cpu调度的最小单元,一个进程中包含多个线程,线程使用进程分配的资源
进程&线程
并发:一段时间内来运行的多个线程
并行:同一时间来运行的线程,有多少核cpu就可以同时并行运行多少个线程
并发&并行
都可以实现线程的阻塞
wait()和sleep需要捕获InterruptedException异常,park()不用
联系
wait()是Object类中的,会释放锁,sleep()是Thread类的静态方法,还是会持有锁。sleep方法只能让当前线程睡眠。调用某一个线程类的对象t.sleep(),睡眠的不是t,而是当前线程。
区别
wait()&sleep()
notify随机唤醒一个线程,notiyAll 唤醒全部线程
unpark()可以唤醒指定线程
在sync里面使用
notiy()¬iyAll()
继承Thread,重写run方法
实现runnable()重写run方法
实现Callable()方法,从写call方法,有返回值,放到线程池,或者Future里
线程池创建线程
通俗说法四种
本质上都是实现Runnable接口
牛逼说法1种
创建方式
new
running
ready
runnable
teminate
blocked
waiting
time_waiting
图片
生命周期
stop() 过时,放弃,因为没有情面可言,直接停止线程,不管你是否在运行状态, 相当于kill -9
线程正常run()结束
修改中断标记状态,将false改为true;唤醒阻塞状态下的线程,并且能被try捕获带中断异常,当异常被捕获到之后,在catch里面会自动的将中断标记复位
thread.isInterrupt() 获取中断标记
interrupted()获取中断标记,然后复位中断标记
通过interrupt(),中断方式
线程的停止
每个线程都有自己的工作线程,通过主内存进行数据传递
通过共享内存的方式
wait()/notiy/caodition.await()/condition.signal()
通过等待通知的方式
通过流的方式
线程通信
原子性,有序性,可见性
互斥
不可剥夺
请求与保持
循环等待
四大条件
对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了
对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了
解决死锁
死锁
跟死锁相反,抢占到共享资源,然后立马释放,不去执行后面的代码
活锁
有些线程的优先级比较高,有些优先级比较低,优先级低的就有可能一直抢占不到,这就是处于饥饿状态
饥饿
活跃性问题
当线程数量大于cpu核心数量的时候,就会带来上下文切换,因为要保存线程状态等操作,所以会造成性能开销
性能问题
带来的问题
CPU100%排查思路?
线程基础
一组操作,要么全部成功,要么全部失败,也是i++
原子性
一个线程对一个共享变量的修改,对其他线程可见
可见性
有代表性的例子就是i++,是由三个指令组成,
有序性
问题
int 变量count++引出思考--》线程安全(原子性)--》解决(sync)-->如何(锁,互斥)-->如何实现互斥--》对象内存布局--》对象头--》锁标识---》锁升级
问题引出?
表示类锁,锁的是类
修饰静态方法
表示对象锁,锁的是对象
修饰实例方法
如果是类名.class 表示类锁
如果是对象引用,则表示锁的是对象
取决于sync修饰的是什么
修饰代码块
使用?锁范围?
对象头(对象标记(Mark Word),和类元信息 (Class Metadata Pointer),实例数据,对齐填充组成
mark word
对象指向方法区,看这个对象属于哪个方法区
类元信息 (Class Metadata Pointer)
数组长度(只有数组对象才有)
对象头
是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
实例数据
齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于32位操作系统要求对象大小是8字节的整数倍,64位的是16字节的整数倍
对齐填充
内存布局图
对象内存布局
hashcode()
分代年龄
偏向锁标记
无锁
轻量级锁
重量级锁
其他锁标记
对象标记(markword)
偏向锁在jdk1.8是默认关闭的,但是面试的时候还是提一嘴吧
当不存在竞争的时候(也就是只有一个线程去抢占共享资源的时候),此时才会有偏向锁
偏向锁
在升级为轻量级锁的时候,第一个线程会修改锁标志,还会将MarkWord里面数据复制到线程的栈中,还会让对象头指向当前的线程,设置成功后表示抢占到锁,在jdk1.6以前,默认轻量级锁自旋次数是10次,如果超过这个次数或自旋线程数超过CPU核数的一半,就会升级为重量级锁!当然次数也可以更改
当存在竞争的时候,就不会出现偏向锁,而是直接升级为轻量级锁
轻量级锁
重量级锁就是线程进入到阻塞状态(当要唤醒线程的时候,是需要CPU进行上下文切换的)
当锁竞争激烈等的时间太长那只能使用Monitor监视器 ,基于操作系统的锁达到效果了
javac -p xxx.class 编译,发现编译成指令后每个synchronized修饰的代码块前后都会有加上一个monitorenter 和monitorexit指令, 这其实就对应了我们上面那种加锁逻辑图里的lock 和unlock操作,monitorexit 指令又两次是因为在出现异常的时候我们也需要解锁操作
重量级锁
锁升级
当只有一个线程的时候,默认是偏向锁,并记录偏向锁标记为1,而且记录线程id,当其他线程来竞争的时候,升级为轻量级锁,然后还没抢到的时候,会再次升级为重量级锁。
非公平锁
1. 锁消除JIT编译器在编译的时候,进行逃逸分析。分析synchronized锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况。这时就可以消除该锁了,提升执行效率。编译就不用加入monitorenter和monitorexit指令。
锁消除
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可
锁粗化
ObjectMonitor中有两个队列(EntryList、WaitSet)以及锁持有者Owner标记,其中WaitSet是哪些调用wait方法之后被阻塞等待的线程队列,EntryList是ContentionList中能有资格获取锁的线程队列。当多个线程并发访问同一个同步代码时候,首先会进入EntryList,当线程获得锁之后monitor中的Owner标记会记录此线程,并在该monitor中的计数器执行递增计算代表当前锁被持有锁定,而没有获取到的线程继续在EntryList中阻塞等待。如果线程调用了wait方法,则monitor中的计数器执行赋0运算,并且将Owner标记赋值为null,代表当前没有线程持有锁,同时调用wait方法的线程进入WaitSet队列中阻塞等待,直到持有锁的执行线程调用notify/notifyAll方法唤醒WaitSet中的线程,唤醒的线程进入EntryList中等待锁的获取。除了使用wait方法可以将修改monitor的状态之外,显然持有锁的线程的同步代码块执行结束也会释放锁标记,monitor中的Owner会被赋值为null,计数器赋值为0。如下图所示
实现原理:
synchronize
i++存在指令重排序的问题?---》引出有序性问题----》volatile如何解决有序性问题?--》cpu是如何优化的?
CPU优化之路--》因为cpu与内存存在性能上的严重差距---》引出cpu高速缓存---》出现数据一致性问题--》总线锁,缓存锁(MESI协议)--》性能问题---》storebuffer--->内存屏障(读屏障,写屏障,全屏障)
cpu执行代码的速度>>IO速度------->为了解决前面的问题,提出了高速缓存的概念,但是它又引出了一个新的问题:缓存一致性问题--------->通过上锁去解决(总线锁和缓存锁【MESI】被修改,独享的,共享的,失效的),造成性能问题----->通过写缓存和无效队列去解决性能问题(异步),这里又造成了指令重排的问题,为了解决这个问题---->引入了内存屏障(其实就是禁止写缓存和无效队列的使用)通过读写屏障和全屏障来解决指令重排,通过volatile关键字修饰的变量,jvm会通知底层开启内存屏障
目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题
程序执行顺序性规则 as-if-serial
传递性
volatile 变量规则
thread.join规则
final
sync
start规则
happen-before模型
JMM
volatile不能解决线原子性问题,但是能解决可见性和有序性可见性作用:volatile修饰的变量可以被所有线程可见(强缓存一致性)禁止指令重排,修饰的变量,jvm会通知底层开启内存屏障,禁止指令重排底层:volatile底层的实现其实是通过lock关键字进行实现的volatile的变量在进行写操作时,会在前面加上lock前缀。
volatile
线程安全
并发工具类
通过线程隔离,解决了线程安全问题
是什么?解决了什么?
全局用户信息的使用---》登录的时候将token解析成用户,然后将用户信息存在threadlock里面,下次用的时候也可以更好实现线程隔离。
SimpleDateFormat案列
场景
底层通过ThreadLockMap存储的不同的entry
key存的是threadlocal,而value存的是变量的值
对key进行了弱引用,而entry和value是强引用
底层存储结构?
结构图
反证法
为什么key是弱引用?
线程池中使用 ThreadLocal 为什么可能导致内存泄露呢
如何解决内存泄露问题?
index++
线性探索
左右两边探索
二次探测再散列
threadlocal
开放寻址法
将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
hashmap
2,链地址法:
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
4, 建立公共溢出区
hash冲突解决的方法有四种
Threadlocalmap使用的方式是开放寻找法的线性探索,当index的位置有值的时候,往下index+1下面找,直到找到为空的为止
如何解决hash问题
只要对象还活着,垃圾收集器宁可抛出OOM异常,也不会回收这个对象
我们最常见的引用
强
当内存充足时GC不回收该对象,当内存不充足时回收该对象
引用队列(ReferenceQueue)
软(SoftReference)
不管内存充不充足,只要就行了GC就会回收改对象
ThreadLocal
弱(WeakReference)
虚引用 就是 形同虚设 ,它并不能决定 对象的生命周期。任何时候这个只有虚引用的对象都有可能被回收。因此,虚引用主要用来跟踪对象的回收,清理被销毁对象的相关资源。
虚(PhantomReference)
四种引用方式
设计一个共享变量,拿到可以为1,没抢到为0
1,要实现互斥特性
阻塞队列,FIFO
对特定线程的阻塞和唤醒/LockSupport.park()/unpark();
存储线程ID,看是否是同一个
2,要实现重入特性
逻辑性实现
非公平一上来就就就行cas抢占
3,实现公平与非公平
如何设计一个锁?
void lock();
不遵循设定的公平的规则,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他线程现在在等待队列里了
boolean tryLock()
void lockInterruptibly()
void unlock();
lock接口主要方法
sync/lock
阻塞和唤醒带来的性能劣势
悲观锁
锁住
一般用CAS实现,在一个原子操作内,比较并替换
原子类和并发容器
乐观锁
不锁住
线程要不要锁住同步资源
共享锁/读锁
可以
独享锁/写锁
不可以
多个线程能否共享一把锁
缺点:更慢,吞吐量更小
公平锁
排队
缺点:有可能线程饥饿,也就是某些线程在长时间内,始终得不到执行
先尝试插队,插队失败再排队
多线程竞争时,是否排队
可重入
不可重入
同一个线程是否可以重复获取一把锁
阻塞和唤醒一个java线程需要操作系统切换CPU状态来完成,这种状态转换需要消耗处理器时间
如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
所以避免这些场景,所以使用cas操作,典型应用:AtomicInteger
缺点:没有抢占到锁会一直抢占,会造成开销
自旋锁
自旋
非自旋锁
阻塞
等锁的过程
synchronize就是不可中断锁
locks是中断锁,tryLock(tine)和lockIntrruptibly
可中断锁
不会盲目自旋,会转为阻塞锁
锁自旋/自适应自旋
有些情况下不需要加锁
代码在方法内部的,并且所有同步都是在方法内部的,在这种情况下根本不可能有人来访问我们的方法,jvm会认为它是私有的,不需要加锁
一系列的操作都是对同一个对象反复地加锁解锁加锁解锁,在这种情况下,会将一些列的加锁解锁合为一个加锁解锁。就不用反复申请释放锁了,只需一次就可以全部执行完我们的事项了
JVM层面
1,缩小同步代码块:原子类的就行了
3,减少请求锁的次数
4,避免认为制造热点
5,锁中尽量不要再包含锁
6,选择适当的锁和工具类
我们自己写代码时的优化
锁优化:
锁的类别
闸门,锁只允许一个线程通过,semaphore允许设置的线程数量数通过
2,查看是否阻塞,trylock()/tryacquire()
相同点:
juc下的锁和Semaphore
sync是关键字,在异常时自动释放锁reentrantlock是juc包下的一个工具类
lock更加灵活,但是需要手动释放锁
比较图
和sync的区别
1, 第一个线程过来了(使用的是非公平锁),那么第一个线程thread1会去获取锁.通过CAS的操作,将当前AQS的state由0变成1,证明当前thread1已经获取到锁,并且将AQS的exclusiveOwnerThread设置成thread1,证明当前持有锁的线程是thread1。
简单流程:
加锁过程?
ReentrantLock/Semaphore/CountDownLatch(private static final class Sync extends AbstractQueuedSynchronizer {})
应用:
是AbstractQueuedSynchronizer的缩写,是一个抽象类,是Java并发包 java.util.concurrent 中是一个可以用来构建锁,同步器,协作工具类的工具类。Lock类会有一个AQS类型的属性,来实现锁。实现逻辑是这样子的?
在Semaphore表示“剩余的许可证的数量
CountDownLatch表示:还需要倒数的数量
当state的值0的时候,标识改Lock不被任何线程所任何线程所占有
在ReentrantLock:表示锁的占有情况,包括可重入次数
state
共享变量
保存线程id的setExclusiveOwnerThread
控制线程抢锁和配合的FIFO队列
用来存放\
如果加锁失败,将会自旋,如果自旋还没获得锁的话就会加入到由双向链表组成的阻塞队列中,由尾部加入
由双向链表组成的AQS同步队列
获取操作会依赖state变量,经常会阻塞(比如获取不到锁的时候)
在Semaphore中,获取就是acquire方法,作用是获取一个许可证
在CountDownLatch里面,获取就是await方法,作用是等待,直到倒数结束
期望协作工具类去实现的获取/释放锁等重要方法
AQS一个内部类ConditionObject,它实现了Condition接口,主要用于实现条件锁。
ConditionObject中也维护了一个队列,这个队列主要用于等待条件的成立,当条件成立时,其它线程将signal这个队列中的元素,将其移动到AQS的队列中,等待占有锁的线程释放锁后被唤醒。
Condition典型的运用场景是在BlockingQueue中的实现,当队列为空时,获取元素的线程阻塞在notEmpty条件上,一旦队列中添加了一个元素,将通知notEmpty条件,将其队列中的元素移动到AQS队列中等待被唤醒。
Condition队列
类比
AQS
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做!
某一时刻取出内存值然后在与当前的时刻进行比较,中间存在一个时间差,在这个时间差里就可能会产生 ABA 问题。 ABA 问题的过程是当有两个线程 T1 和 T2 从内存中获取到值A,线程 T2 通过某些操作把内存 值修改为B,然后又经过某些操作将值修改为回值A,T2退出。
在修改变量的时候添加版本号
原子引用类 AtomicStampedReference 来解决这个问题
ABA问题
CAS
ReentrantLock
多线程
0 条评论
回复 删除
下一页