关键字:volatile
在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:
-
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
-
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
-
有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
int i = 0; boolean flag = false; i = 1; //语句1 flag = true; //语句2
对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
先看一段代码,假如线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
可重入锁
可重入锁表示的是,如果一个线程在未释放已获得锁的情况下再次对该对象加锁,将可以加锁成功。而且可以不断的加锁成功多次。但需要注意的是,每次加锁操作必须对应着一次释放锁的操作。
为什么需要可重入锁?先看以下示例(使用内置锁):
以上示例中,a方法调用b方法,两个方法都被内置锁锁定,如果不可重入,那么在调用b的时候当前线程就会等待锁的释放,而实际锁又被自己占用,因此死锁就出现了。而可重入锁就是为了解决这个问题而出现的。
注意:
实际上可重入锁如ReentrantLock在其内部有一个计数器用于保存当前线程对该锁的加锁次数;如果为0是表示当前线程没有获取到该锁。
synchronized隐式锁:
改进后的各种锁:
偏向锁 > 自旋锁 > 轻量级锁 > 重量级锁;按照这种顺序只能一次增加。
偏向锁:偏向于第一个获得它的线程
自旋锁:自旋锁属于一个过渡的锁,是从轻量级锁到重量级锁的过渡,也就是CAS
轻量级锁:当偏向锁不满足条件时,多线程争夺同一锁对象时,并发不大的情况下使用轻量级锁
重量级锁:重量级锁即为Monitor功能的锁(synchronized)
按照锁的特性分类:
悲观锁:独占锁,会导致所有需要锁的线程都挂起,等待持有锁的线程释放锁,它的看法比较悲观;悲观锁认为对于同一数据的并发操作都会发生修改。对于同一并发的数据进行加锁操作。列如synchronized的底层实现方式;
乐观锁:每次不是加锁而是假设没有冲突的试探的去完成操作,若有冲突不断重试直到成功;例如:CAS自旋锁的操作,实际上并没有加锁。
按照锁的顺序分类:
公平锁:公平锁是根据多个线程按照申请锁的顺序获取锁。Java可以通过ReentrantLock对象指定公平
非公平锁:非公平锁不是按照申请锁的顺序获取锁,有可能后申请的锁比先申请的锁优先获取锁。使用synchronized是无法指定公平与否的,它是不公平的。可以通过ReentrantLock对象指定非公平,默认是非公平的。
独占锁(排他锁)/共享锁:
独占锁/排他锁:指该锁只能被一个线程所持有。对ReentrantLock 和 synchronized而言都是独占锁。
共享锁:是指该锁可以被多个线程持有。对ReentrantReadWriteLock而言,其读锁是共享锁,其写锁是独占锁。读写的共享性可保证并发读是非常高效的,读写、写写,写读都是互斥的。
Java中一般分为互斥锁(独占锁)/读写锁(共享锁)
Lock显式锁:
Lock是一个接口,实现类常见的有:
- 重入锁(ReentrantLock)
- 读锁(ReadLock)
- 写锁(WriteLock)
实际基本上通过聚合了一个同步器(AQS——>AbstractQueuedSynchronizer)的子类完成线程访问控制。
Lock的操作借助与内部类Sync,Sync是集成AQS类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Ewbg2Ql-1631859549344)(C:\Users\luoleixing\AppData\Roaming\Typora\typora-user-images\image-20210917112538074.png)]
自旋锁是什么:
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
自旋锁存在的问题
使用自旋锁会有以下一个问题:
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
- 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
这里拿 AtomicBoolean 来举个例子,因为它足够简单。
public class AtomicBoolean implements java.io.Serializable {
private static final long serialVersionUID = 4654671469794556979L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final boolean get() {
return value != 0;
}
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
}
这是 AtomicBoolean 的部分代码,我们看到这里面又几个关键方法和属性。
1、使用了 sun.misc.Unsafe 对象,这个类提供了一系列直接操作内存对象的方法,只是在 jdk 内部使用,不建议开发者使用;
2、value 表示实际值,可以看到 get 方法实际是根据 value 是否等于0来判断布尔值的,这里的 value 定义为 volatile,因为 volatile 可以保证内存可见性,也就是 value 值只要发生变化,其他线程是马上可以看到变化后的值的;下一篇会讲一下 volatile 可见性问题,欢迎关注
3、valueOffset 是 value 值的内存偏移量,用 unsafe.objectFieldOffset 方法获得,用作后面的 compareAndSet 方法;
4、compareAndSet 方法,这就是实现 CAS 的核心方法了,在使用 AtomicBoolean 的这个方法时,只需要传递期望值和待更新的值即可,而它里面调用了 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是个 native 方法,用 c++ 实现,具体的代码就不贴了,总之是利用了 CPU 的 cmpxchg 指令完成比较并替换,当然根据具体的系统版本不同,实现起来也有所区别,感兴趣的可以自行搜一下相关文章。
使用场景
- CAS 适合简单对象的操作,比如布尔值、整型值等;
- CAS 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大;
ABA问题
CAS 存在一个问题,就是一个值从 A 变为 B ,又从 B 变回了 A,这种情况下,CAS 会认为值没有发生过变化,但实际上是有变化的。对此,并发包下倒是有 AtomicStampedReference 提供了根据版本号判断的实现,可以解决一部分问题。
Java可重入锁详解:
Lock接口:
Lock接口提供了lock和unlock方法,提供加锁和释放锁的语义,lockInterruptibly方法可以响应中断,lock方法会阻塞线程直到获取到锁,而tryLock方法则会立刻返回,返回true代表获取锁成功,而返回false则说明获取不到锁。newCondition方法返回一个条件变量,一个条件变量也可以做线程间通信来同步线程。多个线程可以等待在同一个条件变量上,一些线程会在某些情况下通知等待在条件变量上的线程,而有些变量在某些情况下会加入到条件变量上的等待队列中去。
ReentrantLock实现Lock接口:
在Java同步框架AbstractQueuedSynchronizer提到了两个概念,一个是独占锁,一个是共享锁,所谓独占锁就是只能有一个线程获取到锁,其他线程必须在这个锁释放了锁之后才能竞争而获得锁。而共享锁则可以允许多个线程获取到锁。具体的分析不再本文的分析范围之内。
ReentrantLock翻译过来为可重入锁,它的可重入性表现在同一个线程可以多次获得锁,而不同线程依然不可多次获得锁,这在下文中会进行分析。下文会分析它是如何借助AQS来实现lock和unlock的,本文只关注核心方法,比如lock和unlock,而不会去详细的描述所有的方法。ReentrantLock分为公平锁和非公平锁,公平锁保证等待时间最长的线程将优先获得锁,而非公平锁并不会保证多个线程获得锁的顺序,但是非公平锁的并发性能表现更好,ReentrantLock默认使用非公平锁。下面分公平锁和非公平锁来分析一下ReentrantLock的代码。
锁Sync
Sync有一个抽象方法lock,其子类FairSync和NonfairSync分别实现了公平上锁和非公平上锁。nonfairTryAcquire方法用于提供可重入的非公平上锁,之所以把它放在Sync中而不是在子类NonfairSync中(FairSync中有公平的可重入上锁版本的实现),是因为nonfairTryAcquire不仅在NonfairSync中被使用了,而且在ReentrantLock.tryLock里面也使用到了。对于它的分析留到NonfairSync里面再分析。
公平锁FairSync
公平锁的tryAcquire实现和非公平锁的tryAcquire实现的区别在于:公平锁多加了一个判断条件:hasQueuedPredecessors,如果发现有线程在等待获取锁了,那么就直接返回false,否则在继承尝试获取锁,这样就保证了线程是按照排队时间来有限获取锁的。而非公平锁的实现则不考虑是否有节点在排队,会直接去竞争锁,如果获取成功就返回true,否则返回false。
当然,这些分支执行的条件是state为0,也就是说当前没有线程独占着锁,或者获取锁的线程就是当前独占着锁的线程,如果是前者,就按照上面分析的流程进行获取锁,如果是后者,则更新state的值,如果不是上述的两种情况,那么直接返回false说明尝试获取锁失败。
非公平锁NonfairSync
公平锁的lock使用了AQS的acquire,而acquire会将参与锁竞争的线程加入到等待队列中去按顺序获得锁,队列头部的节点代表着当前获得锁的节点,头结点释放锁之后会唤醒其后继节点,然后让后继节点来竞争获取锁,这样就可以保证锁的获取是按照一定的优先级来的。而非公平锁的实现则会首先尝试去竞争锁,如果不成功,再走AQS提供的acquire方法,
非公平锁的tryAcquire方法使用了父类的nonfairTryAcquire方法来实现。
ReadWriteLock接口:
ReadWriteLock是读写锁,可以对共享变量的读写提供并发支持,ReadWriteLock接口的两个方法分别返回一个读锁和一个写锁。
ReentrantReadWriteLock实现ReadWriteLock接口:
ReentrantReadWriteLock即可重入读写锁,下文将分析它在什么情况下是可重入的,而在什么情况下是独占的。ReentrantReadWriteLock 类实现了ReadWriteLock接口,它提供的读写锁是分离的,读锁和写锁分别是独立的锁。而读锁和写锁的实现也是不一样的,ReentrantReadWriteLock使用了两个内部类ReadLock和WriteLock来分别表示读锁和写锁,而这两种锁又依赖于基于AQS的类Sync来实现,Sync也是一个内部类,它继承了AQS类来实现了lock和unlock的语义。
tryAcquire和tryAcquireShared方法
ReadLock在调用lock方法的时候,会调用AQS的releaseShared方法,而releaseShared方法会调用AQS的tryReleaseShared方法,而tryReleaseShared方法在Sync中被重写了。WriteLock在lock的时候,会调用AQS的acquire方法,而acquire方法会调用AQS的tryAcquire方法,而tryAcquire方法在Sync中被重写了,所以接下来分析一下这两个被重写的方法来认识一下WriteLock和ReadLock是如何通过AQS来lock的。
FairSync和NonFairSync的实现
这两个类仅仅是继承了父类然后实现了两个抽象方法,来表示读线程是否需要阻塞和写线程是否需要阻塞,以这样的方式来达到公平锁和非公平锁的目的。
Java中synchronized关键字:
1、synchronized 同步语句块原理
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置, monitorexit
指令则指明同步代码块的结束位置。**
当执行 monitorenter
指令时,线程试图获取锁也就是获取 对象监视器 monitor
的持有权。
在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个
ObjectMonitor
对象。另外,
wait/notify
等方法也依赖于monitor
对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify
等方法,否则会抛出java.lang.IllegalMonitorStateException
的异常的原因。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
(synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。)
2、synchronized 修饰方法原理
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
(synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。)
不过两者的本质都是对对象监视器 monitor 的获取。
Java对象头
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
synchronized
用的锁是存在Java对象头里的。
Hotspot 有两种对象头:
- 数组类型,如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头
- 非数组类型:如果对象是非数组类型,则用2字宽存储对象头。
对象头由两部分组成
- Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容。
- Klass Pointer:类型指针指向它的类元数据的指针。
64 位虚拟机 Mark Word 是 64bit,在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
监视器(Monitor)
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
- MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;
- MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;
那什么是Monitor?可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的。
synchronized优化
从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
1、偏向锁
偏向锁是JDK6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。
偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着, 如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
下图中的线 程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程:
2、轻量级锁
引入轻量级锁的主要目的是 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。
(1)轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成 功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
下图是 两个线程同时争夺锁,导致锁膨胀的流程图:
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时, 都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
锁的优缺点比较
各种锁并不是相互代替的,而是在不同场景下的不同选择,绝对不是说重量级锁就是不合适的。每种锁是只能升级,不能降级,即由偏向锁->轻量级锁->重量级锁,而这个过程就是开销逐渐加大的过程。
如果是单线程使用,那偏向锁毫无疑问代价最小,并且它就能解决问题,连CAS都不用做,仅仅在内存中比较下对象头就可以了;
如果出现了其他线程竞争,则偏向锁就会升级为轻量级锁;
如果其他线程通过一定次数的CAS尝试没有成功,则进入重量级锁;
锁的优缺点的对比如下表:
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法仅有纳米级的差距 | 如果线程间存在锁的竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问的同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间 同步响应非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量 同步块执行速度较长 |
总结和对比(隐式锁/显示锁):
- 隐式锁基本上没有灵活性可言,因为synchronized控制的代码无法跨方法,修饰范围很窄;而显示锁本身就是一个对象,可以充分发挥面向对象的灵活性,完全可以在一个方法里获取锁,另一个方法释放锁。
- 隐式锁简单易用且不会导致内存泄露;而显示锁由程序员控制容易导致内存泄漏;
- 隐式锁只是非公平锁;显示锁支持公平锁也支持非公平锁;
- 隐式锁无法限制等待时间,无法对锁的信息进行监控;而显示锁提供了足够多的方法来灵活的功能;
- 一般来说我们默认情况下使用隐式锁,只在需要显示锁的特性的时候选用显示锁
volatile
volatile关键字被称为轻量级的synchronized,实际上是两个完全不同的东西。synchronized通过的是jvm层面的管理隐式的加了锁。而volatile关键字则是另一个角度,jvm采用相应的手段:
- 被它修饰的变量可见性,线程对变量进行修改后直接写回主内存;
- 线程对变量读取的时候,要从主内存读取而不是缓存;
- 在它修饰变量上的操作禁止指令重排序。
CAS
CAS是cpu的指令,也不属于加锁,它通过假设没有冲突而试探性的去完成操作,如果失败就重试直到成功完成操作。实际我们很少直接使用到CAS,但是Java里提供的原子变量类,就是juc包下的Automicxxx类,这些类的底层实现直接使用了CAS操作来保证使用这些类型的变量,操作都是原子操作,当使用他们作为共享变量的时候,也不存在线程安全问题了。
参考:java里的锁总结(synchronized隐式锁、Lock显式锁、volatile、CAS) - Life_Goes_On - 博客园 (cnblogs.com)