由于在多线程中为了保证数据的原子性使用了Synchronized,为了保证有序性和可见性使用Volatile。 在jdk1.5引入了JUC(java.util .concurrent工具包)。
CAS
(1)概念
CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
CAS是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子的更新某个位置的值,其实现方式是基于硬件平台的汇编指令,再Intel的CPU中,使用的是cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。
当多个线程同时使用CAS操作一个变量的时候,只会有一个胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅被告知失败,并且允许再此尝试,当然也允许实现线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰。
与锁相比,使用CAS会比程序看起来更加复杂一些,但由于其是非阻塞的,它对死锁问题天生免疫,并且线程间的相互影响也非常小,更为重要的是,使用无锁方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁方式拥有更好的性能
简单的说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子。如果变量不是你想要的样子,那么说明已经被别人修改过了,你就需要重新读取,再次尝试修改。
(2)作用和优点
CAS实现起来稍微复杂一些,但是无锁,不需要进行加锁和解锁的过程,不存在阻塞,可以提高CPU的吞吐量。
CAS底层原理
CPU实现原子指令有两种方式
1通过总线锁定来保证原子性
总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器再总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器可以独占共享内存。但是这种方式成本太大。
2 通过缓存锁定来保证原子性
所谓缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在LOCK操作期间被锁定,那么当它执行操作写回内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(与Volatile的可见性原理相同),当其他处理器回写已被锁定的缓存行的数据时,会使缓存无效。
注意:有两种情况下处理器不会使用缓存锁定
1 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行,则处理器会调用总线锁定
2 有些处理器不支持缓存锁定,对于intel486和Pentium处理器,就是在锁定区域在处理器的缓存行也会调用总线锁定
CAS举例
CAS源码分析
java.util.concurrent的 atomic类是通过CAS实现的,下面用AtomicInteger为例来阐述CAS的实现。
public class CAS {
private static int m = 0;
private static AtomicInteger am = new AtomicInteger(0);
public static void increase1(){
m++;
}
public static void increase2(){
am.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
Thread[] tf = new Thread[20];
for (int i = 0; i < 20; i++) {
tf[i] = new Thread(()->{
CAS.increase1();
});
tf[i].start();
tf[i].join(); //join方法加入 当main线程执行遇到tf[i]线程的时候会停下来等待。
// 等tf[i]线程执行完成以后再执行main
}
System.out.println(m);
for (int i = 0; i < 20; i++) {
tf[i] = new Thread(()->{
CAS.increase2();
});
tf[i].start();
tf[i].join(); //join方法加入 当main线程执行遇到tf[i]线程的时候会停下来等待。
// 等tf[i]线程执行完成以后再执行main
}
System.out.println("automic:"+am.get());
}
}
m++自增是由三步实现的 不具有原子性。 incrementAndGet只有一条指令具有原子性
接下来看看AtomicInteger类
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
//后门类调用cpu指令
private static final Unsafe unsafe = Unsafe.getUnsafe();
//地址偏移量
private static final long valueOffset;
static {
try {
//valueOffset为变量在内存中的偏移地址,unsafe就是通过偏移地址得到数据的原值的
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//要修改的值,volatile保证在多线程的环境下看见的是同一个
private volatile int value;
//内部调用unsafe的getAndAddInt方法
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe是CAS的核心类,java无法直接访问底层操作系统,通过本地(native)方法来访问。不过尽管如此 JVM还是开了一个后门:Unsafe,它提供了硬件级别的原子操作
//o为要修改的值 offset为期望值 deltal为目标值
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
//是一个本地的native方法
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
//通过jvm调用底层指令
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
incrementAndGet --> unsafe --> unsafe.cpp -->汇编语句 cmpxchg
cpu硬件支持,CPU指令。 原子性的保证
CAS可以保证一次读---写---改操作是原子操作,在单处理器上该操作容易实现,但在多处理器上实现有点复杂
缓存加锁: 其实针对与上面那种情况我们只需要保证在同一时刻对某个内存地址的操作是原子性的即可。缓存加锁就是缓存在内存区的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不在输出LOCK#信号,而是修改内部的内存地址,利用缓存一致性来保证原子性。缓存一致性机制可以保证同一内存区域的数据仅能被一个处理器修改,也就是说当CPU1修改了缓存行中的i时使用缓存锁定,那么CPU2就不能同时看到缓存了i的缓存行
CAS缺点
CAS虽然高效的解决了原子操作,但是还是存在一些缺陷,主要是:循环时间太长、只能保证一个共享变量的原子操作、ABA问题
循环时间太长 如果CAS一直不成功,如果自旋CAS长时间不成功,则会给CPU带来非常大的开销。在JUC中有些地方限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue
只能保证一个共享变量原子操作 CAS的实现只能针对一个共享变量,如果是多个就只能使用锁,可以尝试把多个变量合并为一个,就可以利用CAS,如 写锁中的state 的高低位
ABA问题 CAS需要检查操作值有没有发生改变,如果没发生改变则更新。但是存在一个情况。如果一个值原来是A,变成了B,然后又变成了A,那么CAS在检查的时候会发现没有改变,但实质上它已经发生了改变。对于ABA的解决方案就是加上版本号,在每个变量上加一个版本号,每次改变加1 即A --> B --> C 变成 1A-->2B-->3C
AQS
(1)概念
AQS(AbstractQueuedSynchronizer),AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类。如果你有看过类似 CountDownLatch 类的源码实现,会发现其内部有一个继承了 AbstractQueuedSynchronizer 的内部类 Sync 。可见 CountDownLatch 是基于AQS框架来实现的一个同步器.类似的同步器在JUC下还有不少。(eg. Semaphore )
java.util.concurrent
(2)基本思想
通过内置得到FIFO(先进先出)同步队列来完成线程争夺的资源管理工作
(3)CLH同步队列
用节点代表线程,每个节点要指向一个前身和一个后继,头节点(同步器:傀儡节点)不是用来存放线程的是用来指向头和尾节点。
队列里面存放的是等待的线程,执行的线程从队列里摘除去了。
每个线程需要 (1)获取锁 (2)释放锁
自旋等待,比用户线程到内核线程的切换会好一些。
AQS用法
自定义锁
public class MyLock implements Lock {
private Helper helper = new Helper();
private class Helper extends AbstractQueuedSynchronizer{
//获取锁
//独占锁
@Override
protected boolean tryAcquire(int arg) {
int state = getState();
if(state == 0){
//利用CAS原理修改state
if(compareAndSetState(0,1)){
//设置当前线程占有资源
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
//释放锁
@Override
protected boolean tryRelease(int arg) {
//获取线程释放后的值
int state = getState() - arg;
boolean flag = false;
//判断释放后是否为0
if(state == 0){
setExclusiveOwnerThread(null);
return true;
}
//没有线程安全问题,在释放锁之前 当前线程已经占有了资源state
setState(state);
return flag;
}
//条件线程
public Condition newConditionObject(){
return new ConditionObject();
}
}
//加锁
@Override
public void lock() {
helper.tryAcquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
helper.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return helper.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return helper.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
helper.release(1);
}
//条件对象
@Override
public Condition newCondition() {
return helper.newConditionObject();
}
}
接下来 测试一下这个方法
public class UseLock {
private MyLock lock = new MyLock();
private int m = 0;
public int next(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return m++;
}
public int lockNext(){
lock.lock();
try {
return m++;
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
UseLock useLock = new UseLock();
Thread[] th = new Thread[20];
for (int i = 0; i < th.length; i++) {
th[i] = new Thread(()->{
System.out.print(useLock.next()+"--");
});
th[i].start();
}
}
}
当测试没加锁的方法时 结果如下。
出现了重复值了。。。线程出问题了。
执行加锁方法结果
未出现数据重复情况。
当执行下面一个测试案例的时候出现问题了
public class UseLock1 {
private MyLock lock = new MyLock();
public void use1(){
lock.lock();
System.out.println("use1");
use2();
lock.unlock();
}
public void use2(){
lock.lock();
System.out.println("use2");
lock.unlock();
}
public static void main(String[] args) {
UseLock1 lock1 = new UseLock1();
new Thread(()->{
lock1.use1();
}).start();
}
}
use1和use2是获取的同一把锁,在use1里面调用use2。按照正常情况同一线程调用同一把锁应该可以重入的,但是实际情况却出现了问题,接下来把MyLock方法修改一下
protected boolean tryAcquire(int arg) {
int state = getState();
if(state == 0){
//利用CAS原理修改state
if(compareAndSetState(0,1)){
//设置当前线程占有资源
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
//如果再次请求的线程是否是当前线程,如果是把传递的值加到当前的state中
}else if (getExclusiveOwnerThread() == Thread.currentThread()){
setState(getState() + arg);
return true;
}
return false;
}
对线程进行判断 如果再次请求锁的线程是当前占有锁的线程,则允许重入。实现了锁的可重入性。
上面是自己实现的锁,那么JDKz中有没有锁呢?当然是有的 ReentrantLock 可重入锁。接下来看一下ReentrantLock这个锁吧
在java.util.concurrent.locks包下
一个可重入互斥Lock具有与使用synchronized
方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。
A ReentrantLock
由线程拥有 ,最后成功锁定,但尚未解锁。 调用lock
的线程将返回,成功获取锁,当锁不是由另一个线程拥有。 如果当前线程已经拥有该锁,该方法将立即返回。 这可以使用方法isHeldByCurrentThread()和getHoldCount()进行检查。
构造方法
创建一个 |
根据给定的公平政策创建一个 |
|
获得锁。 |
|
只有在调用时它不被另一个线程占用才能获取锁。 |
|
如果在给定的等待时间内没有被另一个线程 占用 ,并且当前线程尚未被 保留,则获取该锁( interrupted) 。 |
|
尝试释放此锁。 |
|
获取锁定,除非当前线程是 interrupted 。 |
| |
|
只有在调用时它不被另一个线程占用才能获取锁。 |
|
如果此锁的公平设置为true,则返回 |
|
返回当前拥有此锁的线程,如果不拥有,则返回 |
ReentrantReadWriteLock 读写锁
ReadWriteLock
的一个实现支持类似的语义到ReentrantLock 。
此类具有以下属性:
收购令
此类不会强加读卡器或写入器优先顺序锁定访问。 但是,它确实支持可选的公平政策。
非公平模式(默认)
当被构造为不公平(默认)时,进入读写锁定的顺序是未指定的,受到重入限制。 持续竞争的非空格锁可能无限期地推迟一个或多个读卡器或写入器线程,但通常具有比公平锁定更高的吞吐量。
公平模式
当公平地构建时,线程使用近似到达订单策略来争取进入。 当释放当前持有的锁时,最长等待的单个写入器线程将被分配写入锁定,或者如果有一组读取器线程等待比所有等待写入器线程长的时间,则该组将被分配读取锁定。
尝试获取公平读锁(不可重入)的线程将阻塞,如果写锁定或有等待的写入程序线程。 直到最旧的当前正在等待的写入程序线程获取并释放写入锁之后,该线程才会获取读锁定。 当然,如果一个等待的作家放弃了等待,留下一个或多个阅读器线程作为队列中最长的服务器,其中写锁定空闲,那么这些读取器将被分配读取锁定。
尝试获取公平写入锁(非重入)的线程将阻止,除非读锁定和写锁定都是空闲的(这意味着没有等待线程)。 (请注意,无阻塞ReentrantReadWriteLock.ReadLock.tryLock()和ReentrantReadWriteLock.WriteLock.tryLock()方法不符合此公平的设置,如果可能,将立即获取锁定,而不管等待线程。)
可重入
这把锁既让读者和作家重新获取读取或写入锁在风格ReentrantLock 。 在写入线程所持有的所有写入锁已经被释放之前,不允许非重入读取器。
另外,写入器可以获取读锁,但反之亦然。 在其他应用程序中,当在执行在读锁定下执行读取的方法的调用或回调期间保留写入锁时,重入可能是有用的。 如果读者尝试获取写入锁定,它将永远不会成功。
锁定降级
重入还允许通过获取写入锁定,然后读取锁定然后释放写入锁定从写入锁定到读取锁定。 但是,从读锁定升级到写锁是不可能的。
中断锁获取
读取锁定和写入锁定在锁定采集期间都支持中断。
Condition支持
写入锁提供了一个Condition实现,其行为以同样的方式,相对于写入锁定,为Condition所提供的实施ReentrantLock.newCondition()确实为ReentrantLock 。 这个Condition当然只能用于写锁。
读锁不支持Condition和readLock().newCondition()
投掷UnsupportedOperationException
。
仪器仪表
该类支持确定锁是否被保持或竞争的方法。 这些方法设计用于监视系统状态,而不是进行同步控制。
构造方法
创建一个新的 |
创建一个新的 |
Modifier and Type | Method and Description |
|
返回当前拥有写锁的线程,如果不拥有,则返回 |
|
返回一个包含可能正在等待获取读取锁的线程的集合。 |
|
返回一个包含可能正在等待获取读取或写入锁定的线程的集合。 |
|
返回一个包含可能正在等待获取写入锁的线程的集合。 |
|
返回等待获取读取或写入锁定的线程数的估计。 |
|
查询当前线程对此锁的可重入读取保留数。 |
|
查询为此锁持有的读取锁的数量。 |
|
返回包含可能在与写锁相关联的给定条件下等待的线程的集合。 |
|
返回与写入锁相关联的给定条件等待的线程数的估计。 |
|
查询当前线程对此锁的可重入写入数量。 |
|
查询给定线程是否等待获取读取或写入锁定。 |
|
查询是否有任何线程正在等待获取读取或写入锁定。 |
|
查询任何线程是否等待与写锁相关联的给定条件。 |
|
如果此锁的公平设置为true,则返回 |
|
查询写锁是否由任何线程持有。 |
|
查询写锁是否由当前线程持有。 |
|
返回用于阅读的锁。 |
|
返回一个标识此锁的字符串以及其锁定状态。 |
|
返回用于写入的锁。 |
readLock()采用非锁状态。一般情况下先采用读锁,在执行写操作的时候,先释放读锁,再去加入writeLock(),当写完成之后释放wite.unlock()之前先添加读锁 read.lock(),进行锁降级(由写锁降级到读锁)。不支持锁升级