作者:阳光碎了围脖_182 | 来源:互联网 | 2023-09-02 13:14
文中加入了个人理解,如有不准确的地方欢迎提出,笔者会及时的进行改正。乐观锁与悲观锁乐观锁:假设数据不会发生冲突,只有在进行数据更新的才会对数据进行检查,如果冲突则更新失败并返回错
正文中加入个人理解,如有不准确之处欢迎提出,笔者及时修改。
乐观锁定和悲观锁定乐观锁定:仅在数据更新发生时检查数据,如果发生冲突,更新失败并返回错误消息
悲观锁定:悲观锁定与乐观锁定相反,因为它假设每次都修改资源,所以在访问资源之前先对其进行锁定,以便在其他人尝试访问资源时阻止其直到解除锁定。
比较锁定(cascas )是乐观锁定的实现方法之一。
CAS轻量级锁定在想要更新变量时判断线程内存中的变量与公用内存中的变量的值是否相等,如果相同则进行修正,如果不同,则重新读取变量的值并重复上述操作。
CAS的优缺点CAS的优点很明显,CAS减少了线程之间的上下文切换消耗,避免了线程在一个线程上占用公共资源,并阻止了所有其他线程
从CAS的缺点上图中可以看到,如果其他线程继续修改公共变量,本线程将不断旋转,浪费CPU资源。
说到ABA问题CAS一定要提到ABA问题,首先通过下图来理解什么是ABA问题
线程1获取时变量为a,线程2将变量更新为b,线程3还将变量设置为a。 此时,线程1判断变量是否为原始值并更新为c。
此操作看起来没有问题,即使是变量,对于基本数据类型也几乎没有问题,但如果变量是引用类型,则该字段更改可能会导致严重后果。
那么如何解决ABA问题呢?
通常,添加版本号以确定此公用变量是否已更改。 sql代码示例:
更新表集值=new value,vision=vision1where value=# { old value } and vision=# { vision }在juc中也对应的类3360atomicsttion
publicclassatomictest { publicfinalstaticatomicstampedreferencestringatomic _ reference=newatomicstampedreferencestring (' ) 创建cstaticvoidmain (字符串[ ] args )//线程池threadpoolexecutorthreadpoolexecutor=newthreadpoolexecutor ) 150,150, Executors.defaultThreadFactory ),newthreadpoolexecutor.discard policy ) ); for(intI=0; i 100; I ) { final int num=i; //获取原始versiOnfinalintstamp=atomic _ reference.getstamp (); thread pool executor.execute ((-{ try } thread.sleep ) math.ABS ) int ) ) Math.random ) * 100 ) ); } catch (互联互通) { e.printStackTrace ); 将value与version进行比较,并将其设置为新值if (atomic _ reference.compareandset (ABC )、abc2)、stamp、stamp 1) ) system.out; (); //下一个线程执行后续操作threadPoolExecutor.execute ()-(in tstamp=atomic _ reference.getstamp ),以将数据恢复为原始值; while (! atomic _ reference.compareandset (ABC 2、ABC、stamp、stamp 1); System.out.println ('已恢复为原始值! ' ); ); }} CAS的原子性
p>同时 CAS 的判断与写入操作必须本身保证是原子的,否则在判断和修改变量时其他线程对公共变量进行了修改又会导致数据不安全。
JUC 的 atomic 包下大量使用了 CAS ,我们通过 AtomicInteger 的底层源码来看看 CAS 是如何实现原子性的。
new AtomicInteger().getAndIncrement();
我们点进 getAndIncrement方法
private static final Unsafe U = Unsafe.getUnsafe();public final int getAndIncrement() { return U.getAndAddInt(this, VALUE, 1);}
我们可以看到,这里实际上是调用了 Unsafe 类的方法,Unsafe 类是 Java 的一个后门,由于 java 不能直接操作内存,java 中有许多native 方法用来直接调用 c、c++的方法库从而直接操作内存,Unsafe 中的方法基本都是 native 的。
我们不断再向里点
@HotSpotIntrinsicCandidate public final native boolean compareAndSetInt(Object o, long offset,int expected,int x);
最终发现了一个本地的方法,这个方法调用了调用了 C、C++ 库中的方法。
实际上 C、C++ 库中的方法最终调用的是汇编语言中的 cmpxchg 方法,也就是说 cpu 本身就提供了 CAS 的相关指令。
但是 cmpxchg 指令本身也不能保证原子性,比如两个 cpu 同时进行上述的 cas 操作有可能也会在进行修改的时候被另一个 cpu 打断。但是 C、C++ 库中的方法在 cmpxchg 前加了一个 LOCK_IF_MP 使有多个CPU的时候在加上一把 lock 锁,lock锁会在一条CPU进行CAS 操作的时候锁死总线,这样其他 CPU 就无法操作。
总之 CAS 的原子性底层实现就是通过总线锁实现的。
synchronized
java 中 synchronized 就是悲观锁的一个具体的实现。
我们从 synchronized 锁的对象、底层原理、锁升级来解释。
锁对象分类 对于普通方法,锁是当前对象对于静态方法,锁是当前对象的类的Class对象对于同步代码块,锁是括号中的对象 重量级锁底层原理
首先我们了解一下一个对象在堆内存中存储的结构:对象头、实例数据、对齐填充;
对象头中存储了:对象Hash值、GC年龄计数器、锁的信息、指向对象类型的索引、(数组还会有数组长度)
任何一个对象都有一个 monitor 对象与之关联,而当一个线程拿到锁时,monitor对象的 _Owner 就会指向该线程,其他线程想要拿到锁就会进入 _EntryList 并被阻塞 ,当前线程释放锁后 _EntryList 中的线程会对锁进行争抢(synchronized是一种非公平锁,也就是说不管这个线程是否是先来的,在争抢锁时机会时相同的,而公平锁则是按照先来后到的方式来获取锁)。
对于同步代码块来说,代码最终会编译成 monitorenter 和 monitorexit 指令,monitorenter 对应的就是线程尝试获取锁的过程,而 monitorexit 则是释放锁的过程。
对于方法而言,它的实现方法在 JVM 中没有详细说明,但是也可用通过 monitorenter 与 monitorexit 来实现。
锁的升级
尽管 synchronized 被我们称为是重量级锁,但是自从 jdk1.6 对 synchronized 进行优化后,synchronized 就变得没有那么“重”了。
我们先得出一个结论:能使用 synchronized 就尽量使用 synchronized。
具体的原因是 synchronized 在被优化过后不会一上来就使用重量级锁,而是按照:
偏向锁 -> 轻量级锁 -> 重量级锁
的顺序不断升级。
偏向锁
在有些情况下,锁并不会被不同的线程不断竞争,而是不断被一个线程获取,这样锁的获取和释放就会带来不必要的开销,所以偏向锁就是为了解决这种场景而出现的。
我们在底层原理说到过,对象头中存储了锁的相关信息,其中有 1bit 就是用于存储当前是否是偏向锁(偏向锁表示),2bit用于存储锁标志位(偏向锁对应为 01),在偏向锁的情况下还存储了当前偏向的线程ID,当一个线程想要获取锁首先会判断一下锁对象的偏向线程是否是自己,如果是则代表当前线程已经获取了锁,如果不是则会通过偏向锁标识先判断一下当前是否是偏向锁模式,如果设置了,则尝试使用 CAS 操作将对象头中的线程ID设置为自己的。
偏向锁采用了一种直到产生竞争才会释放锁的机制,也就是说,在尝试使用 CAS 操作将对象头指向自己时其他线程也进行了同样的操作,即产生了冲突,此时偏向锁就会撤销。
撤销的过程:CAS 发生冲突后,拥有偏向锁的线程会在安全点被暂停,此时会检查该线程的状态,如果不在运行则将对象头中的线程置为无锁状态;如果在运行要么对象头重新偏向其他线程,要么恢复到无锁状态,要么标记对象不适合作为偏向锁并升级,最后唤醒暂停的线程。
轻量级锁
轻量级锁的加锁和解锁都运用到了上文提到的 CAS 操作
加锁: 线程在执行同步代码前会先在虚拟机栈的栈帧中创建存储锁记录的空间,并尝试将对象头中的 清脆的翅膀 Work 复制到锁记录中。然后线程会尝试 CAS 操作来将对象头中的指针替换为指向当前锁记录的指针,成功则获取锁,失败则尝试自旋获取锁。
解锁: 尝试将线程中的锁记录替换回对象头,成功则表示没有竞争发生,失败则锁或膨胀为重量级锁(自旋达到一定次数)。
总结
锁优点缺点适用场景偏向锁加锁和解锁不需要额外的消耗,执行速度和非同步代码几乎没有差别如果线程间存在竞争,则带来了撤销锁的消耗只有一个线程访问同步代码轻量级锁竞争的线程不会阻塞,提高了程序的响应速度可能会造成竞争锁带来的自旋,消耗CPU资源追求响应时间,同步代码执行速度快重量级锁线程竞争不会被自旋,不会消耗CPU线程阻塞,相应时间慢追求吞吐量,同步代码执行速度慢可重入锁与不可重入锁 公平锁与非公平锁 自旋锁 互斥锁 读写锁