Volatile变量
valatile是java提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile后,编译器与运行时都会注意到这个变量是共享的,因此不会将变量上的操作与其他内存操作一起重排序【1】。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。虽然它们提供了相似的可见性保证,但不能用于构建原子的符合操作。因此,当一个变量依赖其他的变量时,或者当前变量的新值依赖于旧值时。就不能使用volatile变量
【1】:当线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前对A可见的所有变量的值,在B读取了volatile变量后,对B也是可见的。因此从内存的可见性角度来看,写入valatile变量相当于退出同步块,而读取valatile变量相当于进入同步块。
**加锁机制既可以却被可见性又可以确保原子性,而volatile变量只能确保可见性*0 *
当且仅当满足以下所有条件时,才应该使用volatile变量:
1:对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
2:该变量不会与其他状态变量一起纳入不变性条件中
3:在访问变量时不需要加锁
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210622201316656.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
happens-before
private int a = 0;
private volatile boolean flag = true;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a; // 4
}
}
假设线程 A 执行 writer 方法之后,线程 B 执行 reader 方法。根据 happens-before 原则,这个过程建立的 happens-before 关系为:
1.根据程序次序规则,1 happens before 2; 3 happens before 4。
2.根据 volatile 规则,2 happens before 3。
3.根据 happens before 的传递性规则,1 happens before 4。
这样就能保证线程 B 在 reader 方法中读取 a 变量时能够看见线程 A 在 writer 方法中对 a 的修改,即使在 writer 方法中对变量 flag 的修改同样看似多余。
Happens-Before 的规则
程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行
监视器规则:在监视器上的解锁操作必须在同一个监视器锁上的加锁操作之前执行
volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行
线程启动规则:在线程上对Thread.Start的调用必须在该线程中执行任何操作之前执行
线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive返回false
中断规则:当一个线程在另一个线程在调用了interrupt,必须在背中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)
终结器规则:对象的狗在函数必须在启动该对象的终结器之前执行完成
传递性:如果操作A在操作B之前执行,操作B在操作C之前完成,那么操作A必须在操作C之前完成
server下的jvm和client下的jvm
程序使用哪一种编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,用户可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或者Server模式
Client Compiler(客户端模式下编译器 C1):编译器 可以将”热点代码“变成本地代码,加快运行速度,但是会更加内存消耗。
Server Compiler(服务端模式下编译器 C2):解释器 省去编译时间,立即执行。启动速度快
隐式的逸出对象
public class ThisEscape {
public ThisEscape(EventSource source){
source.registerListener(
new EventListener(){
{
//可能导致在构造函数返回的时候状态不一样的情况。所以又被称为不正确的构造
System.out.println(ThisEscape.this);//存在,不为null
}
public void on Event(Event e) {
doSonething(e);
}
}
)
}
}
当ThisEscape发布EventListener时候,也会隐含地发布了ThisEscape实例本身,因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用
正确的创建方式(工厂模式)
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
do something(e);
}
}
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe
}
}
线程关闭
栈封闭 P36
简单来说就是帮变量,操作的类对象,定义到方法内部,并且不返回。运用jvm本身的特性。局部变量都封装在栈当中,并且不共享。所以这些数据都是安全的。局部内部类又多了一种好处。
ThreadLocal类 P37
这个类能使线程中的某个值(只能一个)与保存值得对象(当前线程名称)关联起来。ThreadLocal提供了get与set等接口或方法,这些方法为每个使用该变量的线程都存在一份独立得副本。因此get总是返回又当前执行线程在调用set时设置得最新值。
不可变
不可变对象一定是线程安全的
当满足以下条件时,对象才是不可变的
1:对象创建之后其状态就不能改变了
2:对象的所有域都是final类型
3:对象是正确的创建(在对象创建期间,this引用没有逸出)
Final域
在java内存模型中,final域能确保初始化过程的安全性
装饰器模式和监视器模式保证线程安全
例如ArrayList和HashMap可以通过类提供了包装器工厂方法(如Collections.synchonizedList等方法)。把对象被一个安全的对象包装起来。然后包装该类的对象,调用方法是安全的。被称作装饰器模式
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021062120002414.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621200056377.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
这里注意到大部分方法用synchronized(一个对象mutex(互斥))同步块
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210621200750783.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
通过这种方法才形成加锁。这种方式就被承做监视器模式。
那么问题又来了?直接锁this和创建一个无用的Object赋值进行上锁,到底有什么区别呢?
在书中这样写道:使用私有的锁的对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁),有许多优点。私有的锁对象可以将锁封装起来,使客户代码(调用者)无法得到锁,但客户代码可以通过公有方法来访问锁【1】,以便(正确或者不正确)参与到它的同步策略中。如果客户代码错误地获得了另一个对象的锁,那么可能会产生活跃性问题【2】.此外,要想验证某个公有访问的锁在程序中是否被正确使用,则需要检查整个程序,而不是单个类
【1】如果是对象的内置锁。以一个单例作为例子。该对象是可以被全有人访到。那么名义上锁也是可被获取的,只是可能不为0
【2】死锁(Deadlock),活锁(Livelock),饥饿
隐藏迭代器
public class HiddenIterator {
private final Set<Integer> set = new HashSet<>();
public synchronized void add(int i) { set.add(i); }
public synchronized void remove(int i) { set.remove(i); }
public void addTenThings() {
Ramdom rand = new Random();
for(int i = 0; i<10; ++i) add(rand.nextInt);
**System.out.println(set);**
}
}
**addTenthings方法可能会抛出ConcurrentModificationException,因为在输出的过程当中,toString对容器进行了迭代。**在调式代码和日志代码中通常会忽视这种要求。正确的做法就在输出的时候获取HinnenIterator的锁
注意:容器的hashCode和equals等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或键值时,就有可能出现这种情况。同样,containsAll,removeAll和retainAll等方法,以及把容器作为参数的构造方法,都会对容器进行隐藏式迭代。
为什么CopyOnWriteArrayList明明上锁了,还需要copy?明明值没变还需要set
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210622194741230.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210622194753741.png)
这里注意要到array 是一个被volatile修饰的属性。那么该属性就存在可见性。关于到内存模型JMM。在CPU想要获取数据的时候,它会现在缓存当中找,因为距离CPU越近,速度越快,L1,L2是CPU自身的 ,L3是一个共享缓存。那么在多个CPU的情况下。当同时检查同一个内存位置时会发生什么?在什么条件下他们会看到相同的值?
在处理器级别,内存模型定义了知道其他处理器写入内存对当前处理器可见,以及当前处理器写入对其他处理器可见的充分必要条件。一些处理器表现出强大的内存模型,其中所有处理器始终看到任何给定内存位置的完全相同的值。其他处理器表现出较弱的内存模型,其中需要称为内存屏障的特殊指令来刷新本地处理器缓存或使本地处理器缓存无效,以便查看其他处理器所做的写入或使其他处理器可见该处理器的写入。这些内存屏障通常在执行锁定和解锁操作时执行
闭锁 CountDownLatch
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021062718070279.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
任务 FutureTask
信号量 Semaphore
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210627175253123.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
栅栏 Barrier
CyclicBarrier可以周期性地(cyclic)创建出屏障(barrier)。在屏障移除之前,碰到屏障的线程无法继续执行。屏障的解除条件是到达屏障处的线程个数到带了构造函数指定的个数。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210712223021878.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210712223042336.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
线程池
线程池 |
特点 |
newFixedThreadPool |
创建一个固定长度的线程池,每当提交一个任务时创建一个线程,直到最大数量不再变化(如果某个线程由于法省了未预期的Exception而结束),那么线程池补充一个新的线程 |
newSingleThreadExecutor |
是一个单线程Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程代替。执行顺序依照(FIFO,LIFO,优先级) |
newScheduledThreadPool |
创建一个固定长度的线程池,而且已延迟或定时的方式来执行任务。类似Timer |
newCachedThreadPool |
可缓存的线程池,如果线程池的当前规模过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程池,线程池的规模不存在限制 |
Executor
public tnterface Executor {
void execute(Runable command)
}
在向线程池添加任务,执行execute时候,不是一定时间执行任务的,只是把任务放到一个阻塞队列当中,让Woker来执行,Woker是实现了Runable 添加了一些加锁的方式。基于生产者-消费者模式。执行阻塞队列中的任务。也就是说线程池是不管理任务执行的,它只掌管任务队列(worker Queue) 和 工作线程(Worker)。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210623205319715.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
Woker只管执行,那么谁管任务的状态。Future和FutureTask
ExectorService 中 execute 和 submit 区别
execute:没有返回值,不知道捕获执行结果,无法进行后续执行
submit:返回Futrue 能够对执行结果进行详细解析。捕获异常。对于添加到任务队列中的任务,有一个明确的对象。
CompletionService 完成任务后自动回调
创建线程池各个属性的含义
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210624214815588.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
corePoolSize:基本线程池大小,一开始全部是未开启的。添加任务就会开启一个线程,直到到达基本线程池大小之前,都会一直创建。不管之前创建的线程是否空闲
maximunPoolSize:最大大小,当基本线程池全部线程处于忙碌状态下,创建新的线程。执行任务。
keepAliveTime:每当Woker执行runWoker时候获取Woker都会调用getTask()。getTask()会判断当前Worker是否已经过期。Poll出
unit:存活时间
workQueue:存取任务的队列
threadFactory:创建工作(Worker)现成的方式。可以设置UncaughtExceptionHandler,定制一些Thread行为。简单来说定制Woker。术业有专攻
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210624220711340.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
可扩展。还有一个terminated,在线程池关闭时候调用
线程活跃性问题
死锁
造成原因:最出名的死锁问题就是哲学家问题。同时获取对象想要的资源不放导致的死锁。造成死锁有四种原因如下
1、互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
2、占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
解决方式:1.按序获取锁,2.开放调用(缩小锁的范围),3.设置定时锁,4.通过线程转储信息分析死锁
饥饿
造成原因:当线程无法访问它所需要的资源而而不能继续执行时,就发生”饥饿“。饥饿有两种情况,第一种该线程执行一个死循环递归。无法正常结束。第二种,线程的优先级不当,只执行优先级高的线程,导致线程低的线程”饥饿“
解决方式:不要试图设计线程优先级,因为在不同的平台,它们的优先级是不一样得,某个系统中两个不同的优先级可能被映射到同一个优先级当中
糟糕的响应性
造成原因:处理一些计算密集行的操作。
解决方式:可以用等级队列对一些计算密集型的操作设置大的等级数(1优先级最高)
活锁
造成原因:当多个相互协作的线程都对彼此进行的相应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就会发生”活锁“。对于礼让
解决方式:添加随机的等待时间
丢失的信号
丢失的信号是指:线程必须等待一个已经为真的条件,但是开始等待之前没有检查条件谓词。
错误如图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/202106271425172.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
正确如图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210627142530706.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
线程引入的开销
上下文切换
当可运行的线程大于CPU的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用CPU。这会导致一次上下文切换,在这个过程中将保存当前运行线程的执行上下文,并将新调度进来的线程的执行上下文设置为当前上下文。当然线程只是程序当中最小的执行单位。真正运行它的还是CPU调度的进程。所以它们使用是相同的CPU,这回导致程序可用CPU时钟周期减少,而且可能所需要的数据还不存在缓存中。因而线程在首次调度运行时会更慢。这就是==为什么调度器会为每个可运行的线程分配一个最小的执行时间。==保证执行一些不会被中断,堵塞(阻塞I/0,等待获取锁,条件上等待)的线程上运行。
内存同步
同步操作的性能开销包括多个方面。在synchronized和volatile提供的可见性保证中可能会使用一些特殊的指令,既内存栅栏。内存栅栏可以刷新缓存,使缓存无效,刷新硬件的写缓存,以及停止执行管道。内存栅栏可能同样会带对性能带来间接的影响,因为它抑制一些编译器优化操作。在内存栅栏中,大多数操作都是不能被重排序的。
public String getStoogeNames() {
List<String> stooges = new Vector<String>();
stooges.add("ABC");
stooges.add("BAC");
stooges.add("CBA");
return stooges.toString();
}
如上的代码,JVM会通过逸出分析来找出不会发布到堆的本地对象引用(因此这个引用是线程本地的)。首先说明该对象是不会在堆当中创建的,只会创建在自己封闭的栈中。所以JVM会默认里面的操作是安全的。取消了获得锁/释放锁的操作
阻塞
当在锁上发生了竞争时,竞争失败的线程肯定会阻塞。JVM在实现阻塞行为时,可以采用自旋等待(不断尝试获取锁,直到成功)或者通过操作系统挂起被阻塞的线程。哪个好,取决于上下文切换的开销以及在成功获得锁之前需要等待的时间。(由于锁竞争而导致阻塞时,线程在持有锁时将存在一定的开销:当它释放锁时,必须告诉操作系统系统恢复运行阻塞的线程)
解决方式
减少锁的竞争
1:减少锁的持有时间
2:降低锁的请求频率
3:使用带有协调机制的独占锁,这些机制允许更高的并发性
第3点需要简单描述一下什么是带有协调机制的独占锁。在每一个Thread中都有一个isInterrupted()判断当前线程是否想要中断。合理的中断机制能让去调度线程。
缩小锁的范围(“快进快出”) 又称为开放调用
见名知意,这也就为什么代码需要高内聚,低耦合,一方面是为了能够合理的复用代码,扩展代码。另一个方面就是可以缩小锁的范围。通过使用线程安全的类,减免一些自己上锁操作
减小锁的粒度
如果一个锁需要保护多个相互独立的状态变量,那么可以将这个锁分为多个锁,并且每个锁只保护一个变量。从而提高可伸缩性,并降低每个锁被请求的频率
锁分段
例如,在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所在散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。正是这项技术使得ConcurrentHashMap能够支持多个16个并发得写入器。
锁分段一个劣势在于:与曹勇单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销很大。当ConcurrentHashMap需要扩展映射范围,以及重新计算键值得散列值要分布更大得桶集合时们,就需要获得获取分段锁集合中所有得锁
避免热点域
在实现HashMap中通常都会在调用add是时候顺便调整size的大小,让O遍历(n)开销O(1)。这种类似的做法被称作“热点域”。在单线程或者采用完全同步的实现中。是能提升执行速度的策略。然而在并发中却导致难以提升实现的可伸缩性,因为每个修改的map操作都需要更新这个共享的计算器。在访问这共享值的时候。都需要使用独占锁。所以存在伸缩性问题。
为了避免这个问题,ConcurrentHashMap中的size将对每个分段进行枚举来将每个分段中的元素数量相加,而不是维护一个全局计算。通过自身的分段锁维护。然后在ConcurrentHashMap获取size,isEmpty
可能会存在误差,但通常是可接收的。
一些替代独占锁的方法
ReadWriteLock
原子变量
显式锁
注意:在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的,可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
ReentrantLock 公平锁和非公平锁差异
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210627174525663.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210627174556999.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
ReentrantReadWriteLock
构建时,可以通过参数创建是否为公平锁策略。
在公平的锁中:等待时间最长的线程将会优先获得锁。如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他读线程都不能获取读取锁,直到写线程使用完并且释放了写入锁。
在非公平锁中:线程获取访问许可的顺序是不确定的。写线程可以降级为读线程,但是从读线程升级为写线程则是不可以的(这会导致死锁)。死锁的原因:当两个读线程想要升级为写线程的时候,它们在争夺写线程中,持有读线程的锁并没有释放。这会导致未能夺锁成功的线程一直持有读线程堵塞。然后获取到写锁的线程又必须等待所有读操作完成,没有了读线程后才进行写入操作
ReadWriteLock使用了一个16位的状态来表示写入锁的计数,并且使用了另一个16位的状态表示读锁的技术。在读取锁上的操作将使用共享的获取方法与释放方法,在写入锁上的操作将使用独占锁的获取方法和释放方法
写上锁
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210627183152324.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
读上锁
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021062718334493.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Nzg0MzMzMg==,size_16,color_FFFFFF,t_70)
重点只有读锁直接返回1,让其他读锁通过
AbstractQueuedSynchronizer(重点类,需要弄懂)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210627171431395.png)
这个整数可以用来表示任意状态。例如ReentrantLock用它来表示所有线程已经重复获取该锁几次,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态(尚未开始,正在运行,已经完成,以及取消)
CAS
CAS(compareandswap)比较和交换,是一种非阻塞算法。包含3个操作数——需要读写的内存位置V,进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。
CAS的主要缺点:它将是调用者处理竞争问题(通过重试,回退,放弃),而在锁中能自动处理竞争的问题(线程在获得锁之前一直堵塞)。CAS最大的缺陷在于难以围绕CAS正确地构建外部算法
ABA 问题
AtomicStampedReference:维护一个“对象-引用”二元组,通过在引用上加上“版本号”,从而避免ABA问题
AtomicMarkableReference:维护一个“对象引用-布尔值”二元组,在某些算法中将通过将这种二元组使节点保存在链表中同时又将其标记为“已经删除节点”。