Java并发编程(二)CAS无锁算法,ABA问题,悲观锁和乐观锁,AQS同步队列,java中的原子操作类

一、CAS无锁算法
1.1 CAS

CAS(Compare and swap),CAS是一种乐观锁

语义:我认为V的值应该是A,如果是A,就将V的值修改为B,否则不修改并告诉V的实际值是多少。
当多个变量同时使用CAS更新某个变量值的时候,只有一个线程会更新成功,其他线程都失败,但是失败的线程不会被挂起,它们可以重新多次尝试修改,直到修改成功。

CAS有三个操作数,内存值V,预期值A,要修改的新值B,当且仅当预期值A
和内存值V相同时,将内存值修改为B,否则什么都不做。
1.2 CAS存在“ABA”漏洞:

    比如,线程1从内存中取出来值A,准备进行修改,但是在这段时间差内,线程2修改了A的值为B,又把B改回了A。当线程1去更新的时候,发现内存中的数据和预期数据一致,可以进行更新,但此时内存中的值已经发生了变化,只是又变回来了而已。

    可能会带来的问题,比如一个栈的元素为B-A,线程1取了栈顶元素A准备进行修改,但这段时间里,线程2对栈进行操作,让A,B依次出栈,让C,D,A 依次进栈,此时线程1回来后,发现栈顶元素还是A,继续操作,而元素B被出栈之后成为了一个游离的对象。

1.3 "ABA"问题JDK是如何解决的?

    ABA问题可以使用JDK的并发包中的  AtomicStampedReference和AtomicMarkableReference  来解决。

// 用int做时间戳

AtomicStampedReference<QNode> tail = new AtomicStampedReference<CompositeLock.QNode>(null, 0);

int[] currentStamp = new int[1];

// currentStamp中返回了时间戳信息

QNode tailNode = tail.get(currentStamp);

tail.compareAndSet(tailNode, null, currentStamp[0], currentStamp[0] + 1)

AtomicStampedReference和AtomicMarkableReference是通过 版本号(时间戳) 来解决ABA问题的,也可以使用 版本号(version) 来解决。

1.4 AtomicStampedReference的源代码是如何实现的?
  • 1.创建一个Pair类来记录对象引用和时间戳信息,采用int作为时间戳,实际使用的时间戳要做成自增的,否则万一时间戳相同的话,还是会有ABA问题。这个Pair对象是不可变对象,所有的属性都是final的,of方法每次返回一个新的不可变对象。
  • 2.使用一个volatile类型的引用指向当前的Pair对象,一旦volatile发生改变,变化对所有线程可见。
  • 3.set方法时,当要设置的对象和当前Pair对象不一样时,新建一个不可变的Pair对象。
  • 4.CAS方法中,只有期望对象的引用和版本号和目标对象的引用和版本号都一样时,才会新建一个Pair对象,然后用新建的Pair对象和原有的Pair对象做CAS操作。
  • 5.实际的CAS操作比较的是当前的Pair对象和新建的Pair对象。
二、悲观锁和乐观锁
2.1 悲观锁

悲观锁是每次都假设最坏的情况,不管并发更新冲突是否会发生,都会使用锁机制。悲观锁会锁住读取的记录,防止其他线程读取和更新这些记录,其他线程会一直堵塞,直到这个线程结束。传统的关系数据库中就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在操作之前先上锁,java中的synchronized关键字也是悲观锁。

2.2 乐观锁

乐观锁认为数据一般情况下都不会冲突,所以在数据提交更新的时候才会正式对数据的冲突进行检测,如果发现冲突,则返回错误信息。

乐观锁的实现方式:
(1)使用数据版本,为数据增加版本标识,一般通过为数据库表增加一个数字类型的“version”字段实现的,当读取数据时,将数据的version字段值一同取出,每更新一次,则version字段值就加1。当提交更新的时候,判断数据库对应记录的当前版本信息与第一次取出来的version值进行比对,如果相等,则正常更新,如果不相等,则认为是过期数据。
(2)同样是需要在乐观锁控制的table上增加一个字段,字段类型使用时间戳,和上面的version类似,也是在提交更新的时候检查当前数据库中的时间戳和自己更新前取到的
时间戳是否一致。

三、AQS同步队列
3.1 AQS

AbstractQueuedSynchronized,抽象的队列式的同步器。AQS定义了一套多线程访问共享资源的同步器框架,许多同步类的实现都依赖于它,如常用的ReentrantLock等。

它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列
在这里插入图片描述
state的访问方式有三种:
getState() setState() compareAndSetState()

3.2 AQS定义两种资源共享方式

Exclusive(独占,只有一个线程能执行,如ReentrantLock)和 Share(共享,多个线程可以同时执行,如Semaphore/CountDownLatch)

不同自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队、唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。
tryAcquire(int):独占方式。尝试获取资源,成功返回true,失败返回false。
tryRelease(int):独占方式。尝试释放资源,成功返回true,失败返回false。
tryAcquiredShared(int):共享方式。尝试获取资源,负数表示失败;0表示成功,
                       但没有剩余可用资源;正数表示成功,且有可用资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待节点返回true,
                      否则返回false。

    以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程Lock()时,会调用tryAcquire()独占锁并将state+1。此后,其他线程再tryRequire()时就会失败,直到A线程unlock()到state为0,其他线程才有机会获得该锁。当然,锁释放之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念,但要注意的是,获取多少次就要释放多少次,这样才能保证state是能回到零态的。

    再以CountDownLatch为例,任务分为N个子线程去执行,state也初始化为N(N要与子线程个数一致),这N个子线程是并行执行的,每个子线程执行完之后countDown()一次,state会CAS减1。等到所有子线程都执行完之后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

3.3 源码解析

3.3.1 、 acquire(int)
此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。

 public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
 }

函数流程如下:

  • 1、tryAcquire() 尝试直接去获取资源,如果成功则返回
  • 2、addWaiter() 将该线程加入等待队列的尾部,并标记为独占模式。
  • 3、acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果再整个等待过程中被中断过,则返回true,否则返回false。
    流程:
    (1)结点进入队尾之后,检查状态,找到安全休息点。
    (2)调用park()进入waiting状态,等待unpark()或者interrupt()唤醒自己。
    (3)被唤醒后,看自己是否有资格拿到号。如果拿到,head指向当前节点,并返回从入队到拿到号的过程中是否被打断;如果没拿到,则继续1
  • 4、如果线程再等待过程中被中断过,它是不响应的,只是获取资源之后才进行selfInterrupt(),将中断补上。
    3.3.2 、release(int)
    此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里其他线程来获取资源。
 public final boolean release(int arg) {
     if (tryRelease(arg)) {
         Node h = head;//找到头结点
         if (h != null && h.waitStatus != 0)
             unparkSuccessor(h);//唤醒等待队列里的下一个线程
         return true;
     }
     return false;
 }

3.3.3 acquireShared(int)
此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。

 public final void acquireShared(int arg) {
     if (tryAcquireShared(arg) < 0)
         doAcquireShared(arg);
 }

流程:

  • 1.tryAcquireShared()尝试获取资源,成功则直接返回-
  • 2.失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并且获取到资源为止再返回。
    其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还回去唤醒后继队友的操作。

3.3.4.releaseShared(int)
它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。

 public final boolean releaseShared(int arg) {
     if (tryReleaseShared(arg)) {//尝试释放资源
         doReleaseShared();      //唤醒后继结点
         return true;
     }
     return false;
 }

转载自:https://www.cnblogs.com/waterystone/p/4920797.html

四、java中的原子操作类

java.util.concurrent.atomic包一共提供了13个类,属于四种类型的原子更新方式:
原子更新基本数据类型,原子更新数组,原子更新引用,原子更新属性。

4.1 原子更新基本类型

java.util.concurrent.atomic包提供了一下3个类:

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型

以上三个类提供的方法一样,现在只分析AtomicInteger:

  • int addAndGet(int delta):以原子方式将输入的值和实例中的值相加,并返回结果。
  • boolean compareAndSet(int expect,int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
  • int getAndIncrement():以原子方式将当前值加1,但是返回的是自增前的值。
  • void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其线程再之后一小段时间内还是可以读到旧的值。
4.2 原子更新数组

java.util.concurrent.atomic包提供了一下3个类:

  • AtomicIntegerArray:原子更新整型数组里的元素。
  • AtomicLongArray:原子更新长整型数组里面的元素。
  • AtomicReferenceArray:原子更新引用类型数组里面的元素。
4.3 原子更新引用类型
  • AtomicReference:原子更新引用类型。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
  • AtomicMarkableReference:原子更新带有指定标记的引用类型。可以原子更新一个布尔类型的标记位和引用类型。
    构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)
4.4 原子更新字段类

如果需要原子更新某个类的字段时,需要使用原子更新字段类。

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型的字段的更新器。
  • AtomicStampedReference:原子更新带有版本号的更新器。该类型将整数值和引用关联起来,可用于原子的更新数据和数据的版本号,可以解决ABA问题。

原子更新类的字段,需要两步:
第一步:因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
第二步:更新类的字段必须使用public volatile修饰。

转载自:https://blog.csdn.net/fjse51/article/details/56842777



【Java面试题与答案】点我☞:一键直达
在这里插入图片描述

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值