高并发编程(CAS、AtomicStampedReference时间戳原子操作类、自旋锁、synchronize锁升级)

谁无风暴劲雨时,守得云开见月明


CAS

CAS是什么?
CAS全程是CompareAndSet,比较转换。是原子操作类的一种方式。达到预期值才修改。否则不修改

代码调用:

public class Test {
     
    public static void main(String[] args) {
     
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        //     public final boolean compareAndSet(int expect, int update) {
     
        // 期望、更新
        // 如果我期望的值达到了,那么就更新,否则就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}

源码分析:

//native关键字是操作内核引擎的关键字,代表操作的是内核系统
//var 1,是当前类
//var 2,位置便宜量
 public native int getIntVolatile(Object var1, long var2);
 //var1,当前类
 //var2,内存位置偏移量
 //var4,修改为的值	
 public final int getAndAddInt(Object var1, long var2, int var4) {
     
        int var5;
        //这里内存加载,速度快。
        do {
     
        	//从内存中根据偏移量中读取数据
            var5 = this.getIntVolatile(var1, var2);
            //判断var5为拿到的值,预期值,var5+var4即修改后的值,。
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

总结:

CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作,如果不是就一直循环!
缺点:

  • 底层的自旋锁,循环耗时
  • 一次性只能保证一个共享变量的原子性,不能锁代码块
  • 会存在ABA问题

AtomicStampedReference时间戳原子类操作对象解决CAS中的ABA

在CAS中我们比较的时候,如果其他线程在我们读取数据正准备修改时将值更改后会造成ABA的问题。
比如:线程A读取到数据X=5,准备修改为X=6,还没有进行修改,这个时候另一个线程将值修改为了X=8,那么X的变化路径为,5=>8=>6,虽然最后的结果是正确了,但是中间发生了变化,万一有其他的线程拿取到了8的值怎么办。

在原子类当中,即采用乐观锁进行判断,即加上乐观锁。AtomicStampedReference中提供了时间错

public class Test {
     
    public static void main(String[] args) {
     
//        AtomicInteger atomicInteger = new AtomicInteger(2020);
		//带时间戳的原子类操作类对象,时间戳设置为1
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);new Thread(()->{
     
        	//拿取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println("a1=>"+stamp);try {
     
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            //预期值,1,修改后的值为2
            System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            //输出版本号
            System.out.println("a2=>"+atomicStampedReference.getStamp());
            //预期值,2,修改后的值为1
            System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            //输出版本号
            System.out.println("a3=>"+atomicStampedReference.getStamp());},"a").start();// 乐观锁的原理相同
        new Thread(()->{
     
            int stamp = atomicStampedReference.getStamp();
            System.out.println("b1=>"+stamp);
            try {
     
            	//停止2秒等待上面的执行完
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            //预期值,1,修改后的值为6,版本后依次加1
            System.out.println(atomicStampedReference.compareAndSet(1, 6, stamp, stamp + 1));
            System.out.println("b2=>"+atomicStampedReference.getStamp());
        },"b").start();
    }
}

上面代码我们启用了2个线程。在最开始值都等于1,后面停止一个线程,让另一个线程对时间戳进行修改。执行结果:
高并发编程(CAS、AtomicStampedReference时间戳原子操作类、自旋锁、synchronize锁升级)_第1张图片
我们发现最后一次为false,因为时间戳发生了改变,版本号预期是2,结果是版本号是3.

自旋锁

就和CAS的源代码一样,不放弃内存操作的时间片段,判断是否和预期值相等,如果预预期值不相等则一直占用则资源
代码实例:

public class TestOne {
     
    public static void main(String[] args) throws InterruptedException {
     
		
		//在外围创建唯一的实例
        Test lock = new Test();
        new Thread(()->{
     
        	//将值写入
            lock.mylock();
            try {
     
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
     
                e.printStackTrace();
            } finally {
     
                lock.myUnlock();
            }
        },"T1").start();TimeUnit.SECONDS.sleep(1);new Thread(()->{
     
            lock.mylock();
            try {
     
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
     
                e.printStackTrace();
            } finally {
     
                lock.myUnlock();
            }
        },"T2").start();
    }
}
class SpinLockTest {
     
		//创建一个原子操作类
       AtomicReference<Thread> atomicReference = new AtomicReference<>();
       // 加锁
       public void mylock(){
     
       		//创建一个线程对象
           Thread thread = Thread.currentThread();
            // 自旋锁
            //将值变为null变为thred
            while (!atomicReference.compareAndSet(null,thread)){
     
            }
            System.out.println(Thread.currentThread().getName()+"===> mylock");
        }
        // 解锁
        public void myUnlock(){
     
            Thread thread = Thread.currentThread();
            System.out.println(Thread.currentThread().getName()+"===> myUnlock");
            //将原子类的数据值为null
            atomicReference.compareAndSet(thread,null);
            }
        }

在上面代码中我们在TestOne 调用方中创建了一个唯一的实例SpinLockTest,内部有一个原子操作类,上锁时我们将值为thread ,线程B进来发现有值了会不断的遍历,阻止线程进行。我们执行myUnlock解锁时,将原子操作类的值置为null,线程B发现到达预期,null=>thread转换成功,拿到锁。
执行结果:
高并发编程(CAS、AtomicStampedReference时间戳原子操作类、自旋锁、synchronize锁升级)_第2张图片

重入锁

当拿到外锁后,内部锁其实可以不用拿取


public class Test {
     
    public static void main(String[] args) {
     
        Phone phone = new Phone();
        new Thread(()->{
     
            phone.message();
        },"A").start();new Thread(()->{
     
            phone.message();
        },"B").start();
    }
}class Phone{
     
    public synchronized void message(){
     
        System.out.println(Thread.currentThread().getName()+"发短信");
        call();
    }
    public synchronized void call(){
     
        System.out.println(Thread.currentThread().getName()+"打电话");
    }
}

上面代表执行call,不会重新去拿synchronize锁。
lock不是可重入锁。
证明文章:https://www.zhihu.com/question/37168009

synchronize锁升级

升级过程

在JDK早期的时候,synchronized的底层实现是重量级的,所谓重量级,就是它直接去找操作系统去申请锁,它的效率是很低的。

JDK后来对synchronized锁进行了优化,这样才有了锁升级的概念。

锁升级的过程大致是这样的:

「偏向锁」 -> 「轻量级锁 (自旋锁)」-> 「重量级锁」

用markword中最低的三位代表锁状态,其中1位是偏向锁位,最后两位是普通锁位。

1.无锁状态= 0 0 1

这个时候没有锁定资源

2.偏向锁 = 1 0 1

这个时候只有一个线程夺得资源,所以升级为偏向锁

3.轻量级锁 = 0 0

当另一个线程开始进入进行竞争的时候synchronization会动态的进行升级为轻量级(自旋锁)

4.重量级锁 = 1 0

因为自旋需要耗费大量cpu的资源,当自旋到一定的层次后,自旋锁就会进行升级为重量级锁(线程等待wait等待唤醒noticeall)

锁升级的流程:
高并发编程(CAS、AtomicStampedReference时间戳原子操作类、自旋锁、synchronize锁升级)_第3张图片
锁的清除调用GC

System.gc();

为什么有自旋锁了还需要重量级锁?

自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。

重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源

偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁。

JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开。

你可能感兴趣的:(Java实战实例,java,多线程,并发编程)